on master help sign in
Merge remote-tracking branch 'origin/develop' into expose-overlay
2e3d492John Ericson
.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+2−3
@@ -2,14 +2,13 @@
2**/.cabal-sandbox
2**/.cabal-sandbox
3**/cabal.sandbox.config
3**/cabal.sandbox.config
4result
4result
5+result-*
5*.o
6*.o
6*.hi
7*.hi
7−
8TAGS
8TAGS
9tags
9tags
10ctags
10ctags
11dist-newstyle
11dist-newstyle
12cabal.project.local
12cabal.project.local
13.ghc.environment.*
13.ghc.environment.*
14−
15−**/ghcid-output.txt
14+**/ghcid-output.txt
.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+73−3
@@ -1,9 +1,79 @@
1−## Contributing to Obelisk
1+# Contribution Guide
2
2
3−Thanks for taking the time to contribute!
3+Contributions and issue reports are encouraged and appreciated!
4
4
5−Before submitting a pull request, please run the tests:
5+- [Opening Issues](#opening-issues)
6+- [Submitting Changes](#submitting-changes)
7+ - [Guidelines for Commit Messages](#guidelines-for-commit-messages)
8+ - [Guidelines for Pull Requests](#guidelines-for-pull-requests)
9+ - [Code Quality](#code-quality)
10+ - [Documentation](#documentation)
6
11
12+## Opening Issues
13+
14+Before opening an issue, please check whether your issue has already been reported. Assuming it has not:
15+
16+* Describe the issue you're encountering or the suggestion you're making
17+* Include any relevant steps to reproduce or code samples you can. It's always easier for us to debug if we have something that demonstrates the error.
18+* Let us know what version of this project you were using. If you're using a github checkout, provide the git hash.
19+
20+## Submitting Changes
21+
22+Most pull requests should target the `develop` branch. `master` is the release branch. `develop` is periodically merged into master after a period of testing.
23+
24+### Guidelines for Commit Messages
25+
26+#### Summary Line
27+The summary line of your commit message should summarize the changes being made. Commit messages should be written in the imperative mood and should describe what happens when the commit is applied. If your commit modifies one of the in-tree haskell packages (found in `./lib`), please prefix your commit summary with the name of the package being modified.
28+
29+One way to think about it is that your commit message should be able to complete the sentence:
30+"When applied, this commit will..."
31+
32+##### Note on bumping dependencies
33+Commits that update a dependency should include some information about why the dependency was updated in the commit message.
34+
35+#### Body
36+For 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.
37+
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+
42+### Code Quality
43+
44+#### Warnings
45+
46+Your pull request should add no new warnings to the project. It should also generally not disable any warnings.
47+
48+#### Build and Test
49+
50+Make sure the project builds and that the tests pass! This will generally also be checked by CI before merge, but trying it yourself first means you'll catch problems earlier and your contribution can be merged that much sooner!
51+
52+You can run the tests like this:
7```bash
53```bash
8$(nix-build -A selftest --no-out-link)
54$(nix-build -A selftest --no-out-link)
9```
55```
56+
57+To test that your changes build across platforms, you can also try to build release.nix, like this:
58+```bash
59+nix-build release.nix
60+```
61+
62+Note, however, that to build release.nix you must accept the android license agreement and your machine must be configured to build both ios and android executables (usually via remote builders).
63+
64+
65+### Documentation
66+
67+#### In the code
68+We're always striving to improve documentation. Please include [haddock](https://haskell-haddock.readthedocs.io/en/latest/index.html) documentation for any added code, and update the documentation for any code you modify.
69+
70+#### In the [Changelog](ChangeLog.md)
71+Add an entry to the changelog when your PR:
72+* Adds a feature
73+* Deprecates something
74+* Includes a breaking change
75+* Makes any other change that will impact users
76+
77+#### In the [Readme](README.md)
78+The readme is the first place a lot of people look for information about the repository. Update any parts of the readme that are affected by your PR.
79+
ChangeLog.mdnew file+111
@@ -0,0 +1,111 @@
1+# Revision history for obelisk
2+
3+This 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.
4+
5+## v0.7.0.1
6+
7+* Fix the version number for `ob` the command-line tool. ([#679](https://github.com/obsidiansystems/obelisk/pull/679))
8+
9+## v0.7.0.0
10+
11+* Fully support HTTP [Range](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range) requests on static assets to support Safari. ([#664](https://github.com/obsidiansystems/obelisk/pull/664))
12+* Support non-EC2 deployments. ([#658](https://github.com/obsidiansystems/obelisk/pull/658))
13+* Fix `ob deploy test android` to work. ([#645](https://github.com/obsidiansystems/obelisk/pull/645))
14+* Fix vulnerability where Android deployments would leave signing keys in the nix store which is world readable. ([#645](https://github.com/obsidiansystems/obelisk/pull/645)) (Thanks to [kmicklas](https://github.com/kmicklas) for the report.)
15+* Add `Obelisk.Backend.runBackendWith` to allow several customizations. ([#668](https://github.com/obsidiansystems/obelisk/pull/668), [#644](https://github.com/obsidiansystems/obelisk/pull/644))
16+* Add `ob profile` command to run Obelisk projects with profiling. `ob profile` works like `ob run`, but instead of using `ghci`, it builds an executable that is built with profiling enabled. ([#654](https://github.com/obsidiansystems/obelisk/pull/654))
17+* Obelisk's `default.nix` now exposes `mkAssets` function which is used to construct the assets served by an Obelisk application. ([#651](https://github.com/obsidiansystems/obelisk/pull/651))
18+* Bump reflex-platform to v0.5.2.0. ([#671](https://github.com/obsidiansystems/obelisk/pull/671))
19+
20+## v0.6.0.0 - 2020-02-21
21+
22+* Fix a bug in `Obelisk.Route.Frontend` where `routeLink`, `routeLinkDynAttr`, and `dynRouteLink` would not behave exactly like `<a href="...">` when run by JavaScript. These functions now scroll to the top of the page when the link is clicked. ([#540](https://github.com/obsidiansystems/obelisk/pull/540))
23+* Fix a bug in `ob run`/`ob watch`/`ob repl` where nested Obelisk projects would also be loaded into the session. These are now ignored. ([#652](https://github.com/obsidiansystems/obelisk/pull/652))
24+* Improve behavior of `ob run`/`ob watch`/`ob repl` when multiple packages with the same name are encountered. Obelisk now issues a warning and tells you which one it will use. ([#653](https://github.com/obsidiansystems/obelisk/pull/653))
25+* Removed `Obelisk.Backend.mkRouteToUrl` since it is easily written in terms of `Obelisk.Route.renderObeliskRoute`:
26+
27+ mkRouteToUrl validFullEncoder (k :/ v) = renderObeliskRoute validFullEncoder (FullRoute_Frontend (ObeliskRoute_App k) :/ v)
28+
29+* Add `Obelisk.Backend.renderAllJsPath` to expose URL path to `ghcjs/all.js`. ([#545](https://github.com/obsidiansystems/obelisk/pull/545))
30+* Add argument to `serveDefaultObeliskApp`, `serveObeliskApp`, and `serveGhcjsApp` to take the path to `all.js` instead of hard-coding it. ([#545](https://github.com/obsidiansystems/obelisk/pull/545))
31+
32+## v0.5.0.0 - 2020-02-07
33+
34+* Add `Obelisk.Route.(?/)`, a convenience function for constructing routes nested in `Maybe`. ([#457](https://github.com/obsidiansystems/obelisk/pull/457))
35+* Add local unpacked packages to the `ob run`, `ob watch`, and `ob repl` sessions. Any `.cabal` or hpack package inside the current obelisk project will be loaded into the session. For `ob run`/`ob watch` this means the session will automatically reload when you save a source file in any of those packages. For `ob repl` it means that `:r` will reload changes to any of those packages. There are some edge cases where this integration is still rough. Report any issues you encounter. ([#489](https://github.com/obsidiansystems/obelisk/pull/489))
36+* Add `ob hoogle` command to start a local [Hoogle](https://hoogle.haskell.org/) server for the project. ([#628](https://github.com/obsidiansystems/obelisk/pull/628))
37+* `ob thunk pack` will now attempt to automatically detect if the thunk is a private or public repo. To avoid this detection, specify `--private` or `--public` manually. ([#607](https://github.com/obsidiansystems/obelisk/pull/607))
38+* Fix a bug in the plain git thunk loader for thunks marked as 'private' when the revision is not in the default branch. ([#648](https://github.com/obsidiansystems/obelisk/pull/648))
39+* Improve handling of runtime nix dependencies. This may fix some issues encountered particularly by users on systems other than NixOS.
40+
41+## v0.4.0.0 - 2020-01-10
42+
43+* Bump reflex-platform which, notably, bumps nixpkgs to 19.09. ([#585](https://github.com/obsidiansystems/obelisk/pull/585))
44+* 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))
45+* 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))
46+* 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))
47+* Improve error messaging when a dependency doesn't have the expected `.cabal` or `package.yaml` file. ([#597](https://github.com/obsidiansystems/obelisk/pull/597))
48+* Improve the skeleton in small ways. ([#593](https://github.com/obsidiansystems/obelisk/pull/593), [#589](https://github.com/obsidiansystems/obelisk/pull/589))
49+* Fix `ob` commands to again support running from any subdirectory of an obelisk project ([#591](https://github.com/obsidiansystems/obelisk/pull/591))
50+* 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))
51+
52+## v0.3.0.0 - 2019-12-20
53+
54+* Change the structure of Obelisk routes to use a designated
55+ `FullRoute` type. This combines frontend and backend routes into one
56+ structure. This is a **breaking** change which requires Obelisk apps
57+ to take specific migrations. They are:
58+ * Rewrite the implementation of `_backend_routeEncoder` in
59+ `Backend` to use `mkFullRouteEncoder` instead of
60+ `handleEncoder`. Specifically, the backend and frontend cases of
61+ the top-level `pathComponentEncoder` become the second and third
62+ arguments of `mkFullRouteEncoder` respectively, while the
63+ missing route becomes the first argument. An example of how to
64+ do this is available in [a reflex-examples
65+ commit](https://github.com/reflex-frp/reflex-examples/commits/28f566c3e7dc615578dc74297b7c620c1f13683e).
66+ * Replace type constructions of `InL` with `FullRoute_Backend` and
67+ `InR` with `FullRoute_Frontend`.
68+* Generalised `pathSegmentEncoder`, added `pathFieldEncoder`.
69+* Added some `Prism`s to the encoder library for manipulating `DSum`s.
70+* Add `ob doc` command, which lists paths to haddock documentation for specified packages.
71+* Bump reflex-platform so that obelisk now uses GHC 8.6.5 and the nixos-19.03 nixpkgs set.
72+* Add support in `obelisk-route` for single parameters in URL paths.
73+* Bump reflex-platform so that obelisk now uses reflex-dom 0.5.2.0.
74+* Use a `--pure` nix shell in `ob run` for parity with `ob repl` and more resilience against "works on my machine".
75+* Pin usages of `<nixpkgs>` in obelisk thunks, etc. to the nixpkgs used by the project's obelisk impl.
76+* Backport ACMEv2 support in obelisk server to regain LetsEncrypt account creation.
77+* Enable HTTPS in `ob run`.
78+* `ob run` now handles `ghci` errors better, and includes a custom `ghcid`
79+ version. As a result, you no longer need to have ghcid installed to
80+ use `ob run`, as we provide one for you.
81+* `ob` commands now complain less on systems with umasks other than `0022`.
82+* Ignore package environment files in `ob run` and `ob repl`.
83+* Add `Obelisk.Route.Frontend.routeLinkDynAttr`.
84+
85+## v0.2.0.0 - 2019-8-17
86+
87+* Configs become ByteStrings.
88+* FrontendConfigsT has been changed into ConfigsT and configs are made available via getConfig/getConfigs
89+ * 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`.
90+* The backend no longer runs in BackendConfigsT.
91+* Add tabulation package. See Data.Tabulation for details.
92+* Add encoders for `DMap`, `HasFields` (cf. Data.Tabulation), and JSON.
93+* Use IP address for nginx proxy pass instead of localhost
94+
95+## v0.1.1.0 - 2019-05-17
96+
97+* Fix crashes of Android apps on 32-bit ARM devices.
98+* Provide a way to indicate acceptance of the Android SDK license by passing `config.android_sdk.accept_license = true;` in the arguments to the import of `.obelisk/impl` in the project's `default.nix`.
99+* Add `COMPLETE` pragma to `(:/)`. Using this pattern synonym should no longer generate spurious warnings about non-exhaustive pattern matching.
100+* Make asset path hashing strict (see `Obelisk.Asset.Gather`)
101+* Add the `ob shell` command to enter a nix shell for an obelisk project
102+* Allow skeleton's obelisk to be overridden. This changes the skeleton's default.nix interface: the arguments that it used to take are now part of the new "obelisk" argument.
103+* Removed `MonadIO` from `ObeliskWidget` to prevent accidental IO during prerendering. If you need to do IO in a widget it should be on the right hand side of a `prerender`.
104+* Significantly changed the interface to the "executable config" packages. `obelisk-executable-config-lookup` is a new internal package which looks up all configs in a platform-specific way. `obelisk-executable-frontend` and `obelisk-executable-backend` provide MTL-style monad classes (`HasFrontendConfigs` and `HasBackendConfigs`) which the frontend and backend, respectively, can use to look up configs. This replaces the old `get` function which ran in `IO`.
105+* Add a flag to force thunk packing even if there are unpushed changes in the unpacked thunk.
106+
107+
108+## v0.1.0.0 - 2019-03-29
109+
110+* Use reflex-dom's `HydrationDomBuilder` to "hydrate" statically rendered DOM served by the Obelisk backend (rather than re-creating DOM and replacing it all).
111+* Add `HasCookies` and `CookiesT` to allow `ObeliskWidget`s to access cookies during static and "hydrated" DOM rendering.
FAQ.md+107
@@ -1,6 +1,9 @@
1# Frequently Asked Questions
1# Frequently Asked Questions
2
2
31. [How do I fix invalid entitlements? ](#how-do-i-fix-invalid-entitlements)
31. [How do I fix invalid entitlements? ](#how-do-i-fix-invalid-entitlements)
4+1. [`ob thunk update` or `ob deploy update` fails](#ob-thunk-update-or-ob-deploy-update-fails)
5+1. [How do I fix `Ambiguous module name` errors?](#how-do-i-fix-ambiguous-module-name-errors)
6+1. [What does `ob run` actually do?](#what-does-ob-run-actually-do)
4
7
5### How do I fix invalid entitlements?
8### How do I fix invalid entitlements?
6
9
@@ -13,3 +16,107 @@ When this happens you'll see an error something like this:
13
16
14Fixing the value of `ios.bundleIdentifier` should fix the error.
17Fixing the value of `ios.bundleIdentifier` should fix the error.
15
18
19+### `ob thunk update` or `ob deploy update` fails
20+Whenever an `ob` command fails, try re-running it with `-v`.
21+
22+If you're using a private repo, and you get a failure in nix-prefetch-url, you may need to unpack and repack the thunk. Here's some example output that shows this issue:
23+
24+```
25+Starting Obelisk </nix/store/j8wls8a89xr6s1a47lg6g83gnbdrfd0l-obelisk-command-0.1/bin/.ob-wrapped> args=["deploy","update","-v"] logging-level=Debug
26+Creating process: 'nix-build' './.obelisk/impl' '-A' 'command' '--no-out-link'
27+✔ Built on ./.obelisk/impl [command]
28+Handing off to /nix/store/j8wls8a89xr6s1a47lg6g83gnbdrfd0l-obelisk-command-0.1/bin/ob
29+Starting Obelisk </nix/store/j8wls8a89xr6s1a47lg6g83gnbdrfd0l-obelisk-command-0.1/bin/.ob-wrapped> args=["--no-handoff","deploy","update","-v"] logging-level=Debug
30+Creating process: 'git' 'ls-remote' '--exit-code' '--symref' 'https://github.com/obsidiansystems/some-private-repo.git' 'refs/heads/master'
31+git ls-remote maps: (fromList [],fromList [(GitRef_Branch "master","8546dfc8be9653f20bb26214e0991dbb957cb290")])
32+Latest commit in branch master from remote repo https://github.com/obsidiansystems/some-private-repo.git is 8546dfc8be9653f20bb26214e0991dbb957cb290
33+Creating process: 'nix-prefetch-url' '--unpack' '--type' 'sha256' 'https://github.com/obsidiansystems/some-private-repo/archive/8546dfc8be9653f20bb26214e0991dbb957cb290.tar.gz'
34+error: unable to download 'https://github.com/obsidiansystems/some-private-repo/archive/8546dfc8be9653f20bb26214e0991dbb957cb290.tar.gz': HTTP error 404
35+Process exited with code 1; 'nix-prefetch-url' '--unpack' '--type' 'sha256' 'https://github.com/obsidiansystems/some-private-repo/archive/8546dfc8be9653f20bb26214e0991dbb957cb290.tar.gz'
36+nix-prefetch-url: Failed to determine sha256 hash of URL https://github.com/obsidiansystems/some-private-repo/archive/8546dfc8be9653f20bb26214e0991dbb957cb290.tar.gz
37+✖ Updating thunk ./src to latest
38+```
39+
40+And here's how you can fix it:
41+
42+```
43+ob thunk unpack $SOME_THUNK
44+ob thunk pack $SOME_THUNK
45+```
46+Substitute the directory of your thunk for `$SOME_THUNK`. In the case of `ob deploy update`, `$SOME_THUNK` will be `src` in the deployment directory.
47+
48+(Based on issue #351).
49+
50+### How do I fix `Ambiguous module name` errors?
51+Since obelisk places the common/backend/frontend modules packages into the same ghci, and ghci doesn't "sandbox" them, it is possible to have conflicting module errors inside `ob repl/watch/run` that do not appear when doing a cabal build.
52+
53+You can disambiguate this via [PackageImports](https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/glasgow_exts.html#package-qualified-imports). For instance, if you see
54+```
55+error:
56+ Ambiguous module name ‘Crypto.Hash’:
57+ it was found in multiple packages:
58+ cryptohash-0.11.9 cryptonite-0.25
59+```
60+then specify the package you want in the import, e.g:
61+`import "cryptonite" Crypto.Hash`
62+
63+
64+### What does `ob run` actually do?
65+
66+
67+#### Short version:
68+
69+`ob run` starts a [`ghcid`](https://github.com/ndmitchell/ghcid) process which
70+tries to build your project within a carefully crafted `nix-shell` with all the
71+project's dependencies and,
72+
73+* either displays compilation errors/warnings,
74+* or starts the Obelisk server, which serves:
75+ * your backend's route handlers,
76+ * the static assets,
77+ * the [JSaddle](https://github.com/ghcjs/jsaddle) frontend code (while opening its websocket).
78+
79+#### Longer version:
80+
81+Assuming we are in a project created with `ob init`, `ob run` calls (see
82+`lib/command/src/Obelisk/Command.hs`):
83+
84+ nix-shell --pure -A shells.ghc --run 'ob --no-handoff internal run-static-io <real-run-function>'
85+
86+where
87+
88+* `shells.ghc` is defined in `./default.nix` by importing `./.obelisk/impl/default.nix` which
89+ is `default.nix` in the present (Obelisk) repository,
90+* `run-static-io` is a logging-enabled command wrapper
91+ (cf. `runObelisk` in `lib/command/src/Obelisk/App.hs`).
92+
93+In this case, it runs the function `Obelisk.Command.Command.run` (defined in
94+`lib/command/src/Obelisk/Command/Run.hs`).
95+
96+* It creates a GHCi config from a Nix expression which:
97+ * loads three packages: `backend`, `common`, `frontend`,
98+ * obtains a free port number.
99+* Then runs `ghcid`
100+ * with a command that reruns `Obelisk.Run.run` at each restart (option
101+ `--test`).
102+
103+It is defined in `lib/run/src/Obelisk/Run.hs`:
104+
105+* It creates a thread which starts the main backend.
106+ This runs `runSnapWithCommandLineArgs` and passes routes:
107+ * to the backend (result of the `_backend_run` field of your
108+ implementation of the `Backend fullRoute frontendRoute` record),
109+ * or to `serveDefaultObeliskApp` (`lib/backend/src/Obelisk/Backend.hs`) to
110+ serve static assets.
111+* Starts `runWidget`
112+ which itself runs
113+ [runSettingsSocket](https://hackage.haskell.org/package/warp-3.2.26/docs/Network-Wai-Handler-Warp.html#v:runSettingsSocket)
114+ (the *“TCP listen loop”*):
115+ * it binds to TCP socket, and
116+ * creates the HTTP connection manager
117+ ( [`newManager`](https://hackage.haskell.org/package/http-client-0.6.3/docs/Network-HTTP-Client.html#v:newManager))
118+ * calls `obeliskApp` which
119+ * has a route to serve the [JSaddle](https://github.com/ghcjs/jsaddle) frontend,
120+ * runs the frontend (*on* the server), to produce pre-rendered pages,
121+ * starts the JSaddle web-socket,
122+ * “falls back” to proxying the backend (`fallbackProxy`).
LICENSEnew file+30
@@ -0,0 +1,30 @@
1+Copyright (c) 2020 Obsidian Systems
2+
3+All rights reserved.
4+
5+Redistribution and use in source and binary forms, with or without
6+modification, are permitted provided that the following conditions are met:
7+
8+ * Redistributions of source code must retain the above copyright
9+ notice, this list of conditions and the following disclaimer.
10+
11+ * Redistributions in binary form must reproduce the above
12+ copyright notice, this list of conditions and the following
13+ disclaimer in the documentation and/or other materials provided
14+ with the distribution.
15+
16+ * Neither the name of Elliot Cameron nor the names of other
17+ contributors may be used to endorse or promote products derived
18+ from this software without specific prior written permission.
19+
20+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
README.md+144−27
@@ -1,12 +1,21 @@
1# Obelisk
1# Obelisk
2
2
3+[![Haskell Programming Language](https://img.shields.io/badge/language-Haskell-blue.svg)](http://www.haskell.org)
4+[![BSD3 License](http://img.shields.io/badge/license-BSD3-brightgreen.svg)](https://tldrlegal.com/license/bsd-3-clause-license-%28revised%29)
5+
6+
3Obelisk provides an easy way to develop and deploy your [Reflex](https://github.com/reflex-frp/reflex) project as web apps and as mobile apps.
7Obelisk provides an easy way to develop and deploy your [Reflex](https://github.com/reflex-frp/reflex) project as web apps and as mobile apps.
4
8
5- [Installing Obelisk](#installing-obelisk)
9- [Installing Obelisk](#installing-obelisk)
6- [Developing an Obelisk project](#developing-an-obelisk-project)
10- [Developing an Obelisk project](#developing-an-obelisk-project)
11+ - [Adding Packages](#adding-packages)
12+ - [Adding Package Overrides](#adding-package-overrides)
13+ - [Running over https](#running-over-https)
7- [Deploying](#deploying)
14- [Deploying](#deploying)
8 - [Locally](#locally)
15 - [Locally](#locally)
9− - [EC2](#ec2)
16+ - [Default EC2 Deployment](#default-ec2-deployment)
17+ - [Custom Non-EC2 Deployment](#custom-non-ec2-deployment)
18+ - [VirtualBox Deployment](#virtualbox-deployment)
10 - [From macOS](#from-macos)
19 - [From macOS](#from-macos)
11 - [Deploying an updated version](#deploying-an-updated-version)
20 - [Deploying an updated version](#deploying-an-updated-version)
12- [Mobile](#mobile)
21- [Mobile](#mobile)
@@ -19,13 +28,15 @@ Obelisk provides an easy way to develop and deploy your [Reflex](https://github.
191. Set up nix caches
281. Set up nix caches
20 1. If you are running NixOS, add this to `/etc/nixos/configuration.nix`:
29 1. If you are running NixOS, add this to `/etc/nixos/configuration.nix`:
21 ```
30 ```
22− nix.binaryCaches = [ "https://cache.nixos.org/" "https://nixcache.reflex-frp.org" ];
31+ nix.binaryCaches = [ "https://nixcache.reflex-frp.org" ];
23 nix.binaryCachePublicKeys = [ "ryantrinkle.com-1:JJiAKaRv9mWgpVAz8dwewnZe0AzzEAzPkagE9SP5NWI=" ];
32 nix.binaryCachePublicKeys = [ "ryantrinkle.com-1:JJiAKaRv9mWgpVAz8dwewnZe0AzzEAzPkagE9SP5NWI=" ];
24 ```
33 ```
34+ and rebuild your NixOS configuration (e.g. `sudo nixos-rebuild switch`).
25 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)):
35 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)):
26 ```
36 ```
27− substituters = https://cache.nixos.org https://nixcache.reflex-frp.org
37+ binary-caches = https://cache.nixos.org https://nixcache.reflex-frp.org
28− trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= ryantrinkle.com-1:JJiAKaRv9mWgpVAz8dwewnZe0AzzEAzPkagE9SP5NWI=
38+ binary-cache-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= ryantrinkle.com-1:JJiAKaRv9mWgpVAz8dwewnZe0AzzEAzPkagE9SP5NWI=
39+ binary-caches-parallel-connections = 40
29 ```
40 ```
30 * 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)
41 * 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)
31 ```
42 ```
@@ -46,16 +57,38 @@ Obelisk provides an easy way to develop and deploy your [Reflex](https://github.
46
57
47When developing on obelisk itself you may launch `ghcid` for the corresponding project as follows. For example to launch ghcid for `lib/backend` project:
58When developing on obelisk itself you may launch `ghcid` for the corresponding project as follows. For example to launch ghcid for `lib/backend` project:
48
59
49−```
60+```bash
50nix-shell -A obeliskEnvs.obelisk-backend --run "cd lib/backend && ghcid -c 'cabal new-repl'"
61nix-shell -A obeliskEnvs.obelisk-backend --run "cd lib/backend && ghcid -c 'cabal new-repl'"
51```
62```
52
63
53Or to launch ghcid for `lib/command` project:
64Or to launch ghcid for `lib/command` project:
54
65
55−```
66+```bash
56nix-shell -A obeliskEnvs.obelisk-command --run "cd lib/command && ghcid -c 'cabal new-repl'"
67nix-shell -A obeliskEnvs.obelisk-command --run "cd lib/command && ghcid -c 'cabal new-repl'"
57```
68```
58
69
70+To re-install `ob` from source do
71+```bash
72+nix-env -f /path/to/obelisk -iA command
73+```
74+
75+Note that `ob` will defer to the version found in your project's `.obelisk/impl` directory. To update that version specifically:
76+
77+```bash
78+ob thunk unpack ./.obelisk/impl
79+cd ./.obelisk/impl
80+# apply your changes
81+```
82+
83+If you want to commit your changes, first push them to your fork of obelisk and then
84+
85+```bash
86+cd /your/project/root
87+ob thunk pack .obelisk/impl
88+git add .obelisk/impl
89+git commit -m "Bump obelisk"
90+```
91+
59### Accessing private repositories
92### Accessing private repositories
60
93
61To allow the Nix builder to access private git repositories, you must be set up
94To allow the Nix builder to access private git repositories, you must be set up
@@ -69,44 +102,103 @@ access to:
69
102
70To create a new Obelisk project, go to an empty directory and run:
103To create a new Obelisk project, go to an empty directory and run:
71
104
72−```
105+```bash
73ob init
106ob init
74```
107```
75
108
76Obelisk leverages ghcid to provide a live-reloading server that handles both frontend and backend. To run your Obelisk app and monitor the source for changes:
109Obelisk leverages ghcid to provide a live-reloading server that handles both frontend and backend. To run your Obelisk app and monitor the source for changes:
77
110
78−```
111+```bash
79ob run
112ob run
80```
113```
81
114
82−Now go to http://localhost:8000 (or the port specified in `config/common/route`) to access your app.
115+Now, with an appropriate browser, go to http://localhost:8000 (or the address/port specified in `config/common/route`) to access your app.
116+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.
83
117
84Every 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.
118Every 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.
85
119
120+### Adding packages
121+
122+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.
123+
124+### Adding package overrides
125+
126+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:
127+
128+```nix
129+# ...
130+project ./. ({ pkgs, ... }: {
131+# ...
132+ overrides = self: super: let
133+ aesonSrc = pkgs.fetchFromGitHub {
134+ owner = "obsidiansystems";
135+ repo = "aeson-gadt-th";
136+ rev = "ed573c2cccf54d72aa6279026752a3fecf9c1383";
137+ sha256 = "08q6rnz7w9pn76jkrafig6f50yd0f77z48rk2z5iyyl2jbhcbhx3";
138+ };
139+ in
140+ {
141+ aeson = self.callCabal2nix "aeson" aesonSrc {};
142+ waargonaut = self.callHackageDirect {
143+ pkg = "waargonaut";
144+ ver = "0.8.0.1";
145+ sha256 = "1zv28np3k3hg378vqm89v802xr0g8cwk7gy3mr77xrzy5jbgpa39";
146+ } {};
147+ };
148+# ...
149+```
150+
151+For further information see [the Haskell section](https://nixos.org/nixpkgs/manual/#users-guide-to-the-haskell-infrastructure) of nixpkgs Contributors Guide.
152+
153+### Adding extra local packages
154+
155+If the standard packages (`frontend`, `backend`, and `common`) are not
156+enough, to add more local Haskell packages, define them with the
157+`packages` parameter. The sources of these packages will be
158+automatically reloaded by `ob run`.
159+
160+```nix
161+# ...
162+project ./. ({ pkgs, ... }: {
163+# ...
164+ packages = {
165+ another = ./another;
166+ };
167+# ...
168+```
169+
170+### Running over HTTPS
171+
172+To run your app locally over https, update the protocol in `config/common/route` to `https`, and then use `ob run` as normal.
173+
174+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.
175+
86## Deploying
176## Deploying
87
177
88### Locally
178### Locally
89
179
90Build everything:
180Build everything:
91
181
92−```
182+```bash
93−nix-build -A exe -o result-exe
183+nix-build -A exe --no-out-link
94```
184```
95
185
96−Run the server:
186+Copy the result to a new directory, add configuration, and run!
97
187
98−```
188+```bash
99−cd result-exe
189+mkdir test-app
190+ln -s $(nix-build -A exe --no-out-link)/* test-app/
191+cp -r config test-app
100−./backend
192+(cd test-app && ./backend)
101```
193```
102
194
103−### EC2
195+### Default EC2 Deployment
104
196
105−In this section we will demonstrate how to deploy your Obelisk app to an Amazon EC2 instance.
197+In this section we will demonstrate how to deploy your Obelisk app to an Amazon EC2 instance. Obelisk deployments are configured for EC2 by default (see [Custom Non-EC2 Deployment](#custom-non-ec2-deployment)).
106
198
107First create a new EC2 instance:
199First create a new EC2 instance:
108
200
109−1. Launch a NixOS 17.09 EC2 instance (we recommend [this AMI](https://console.aws.amazon.com/ec2/v2/home?region=us-east-1#LaunchInstanceWizard:ami=ami-40bee63a))
201+1. Launch a NixOS 19.09 EC2 instance (we recommend [this AMI](https://console.aws.amazon.com/ec2/v2/home?region=us-east-1#LaunchInstanceWizard:ami=ami-00a8eeaf232a74f84))
1101. In the instance configuration wizard ensure that your instance has at least 1GB RAM and 10GB disk space.
2021. In the instance configuration wizard ensure that your instance has at least 1GB RAM and 10GB disk space.
1111. When prompted save your AWS private key (`~/myaws.pem`) somewhere safe. We'll need it later during deployment.
2031. When prompted save your AWS private key (`~/myaws.pem`) somewhere safe. We'll need it later during deployment.
1121. Go to "Security Groups", select your instance's security group and under "Inbound" tab add a new rule for HTTP port 80 and 443.
2041. Go to "Security Groups", select your instance's security group and under "Inbound" tab add a new rule for HTTP port 80 and 443.
@@ -129,9 +221,11 @@ ob deploy init \
129 ~/code/myapp-deploy
221 ~/code/myapp-deploy
130```
222```
131
223
132−NOTE: HTTPS is enabled by default; to disable https, pass `--disable-https` to the `ob deploy init` command above.
224+HTTPS is enabled by default; to disable HTTPS pass `--disable-https` to the `ob deploy init` command above.
225+
226+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.
133
227
134−Then go to that created deployment configuration directory, and initiate the deployment:
228+Next, go to the deployment directory that you just initialized and deploy!
135
229
136```
230```
137cd ~/code/myapp-deploy
231cd ~/code/myapp-deploy
@@ -142,6 +236,26 @@ ob deploy push
142
236
143At this point you are done. Your app will be accessible at `${ROUTE}`. The currently deployed version - the git commit hash of the source repo - can be found at `${ROUTE}/version`.
237At this point you are done. Your app will be accessible at `${ROUTE}`. The currently deployed version - the git commit hash of the source repo - can be found at `${ROUTE}/version`.
144
238
239+### Custom Non-EC2 Deployment
240+
241+By default Obelisk deployments are configured for NixOS machines running on AWS EC2. To provide your own configuration, you need to write a custom `module.nix` in the deployment repository. This still requires that your server is running NixOS.
242+
243+`module.nix` must contain a Nix *function* that produces a [NixOS module function](https://nixos.org/nixos/manual/index.html#sec-writing-modules). The top-level function takes deployment configuration as arguments: `hostName`, `adminEmail`, `routeHost`, `enableHttps`, `version`, `exe`, `nixosPkgs`. Most of these are the values you specified during `ob deploy init` and are stored in the deployment repository. `version` is a `git` hash for the app that you're deploying. `exe` is the Linux build of the app (as seen in [Deploying Locally](#locally)). `nixosPkgs` is the package set used to construct the NixOS VM.
244+
245+The [VirtualBox Deployment](#virtualbox-deployment) section provides an example.
246+
247+#### VirtualBox Deployment
248+
249+Here's a `module.nix` that is configured for deployment to a VirtualBox VM (running NixOS):
250+
251+```nix
252+{ nixosPkgs, ... }: {...}: {
253+ imports = [ (nixosPkgs.path + /nixos/modules/virtualisation/virtualbox-image.nix) ];
254+}
255+```
256+
257+The `{...}:` and following is the [NixOS module](https://nixos.org/nixos/manual/index.html#sec-writing-modules) definition.
258+
145### From macOS
259### From macOS
146
260
147Deploying from macOS requires some extra setup:
261Deploying from macOS requires some extra setup:
@@ -155,7 +269,7 @@ Running `ob deploy push` will give you additional setup instructions.
155
269
156If you'd like to deploy an updated version (with new commits) of your Obelisk app: simply go to the configuration directory, update the source thunk and push:
270If you'd like to deploy an updated version (with new commits) of your Obelisk app: simply go to the configuration directory, update the source thunk and push:
157
271
158−```
272+```bash
159cd ~/code/myapp-deploy
273cd ~/code/myapp-deploy
160ob deploy update
274ob deploy update
161ob deploy push
275ob deploy push
@@ -181,14 +295,14 @@ These versions will work out of the box but iOS SDKs prior to 11.3 should also w
181
295
182More recent Xcodes should also work, as long as one of the SDKs mentioned above has been used.
296More recent Xcodes should also work, as long as one of the SDKs mentioned above has been used.
183To add another SDK to your current Xcode, [download](https://developer.apple.com/download/more/) the corresponding Xcode, extract it and copy its SDK folder next to the installed one, e.g.
297To add another SDK to your current Xcode, [download](https://developer.apple.com/download/more/) the corresponding Xcode, extract it and copy its SDK folder next to the installed one, e.g.
184−```
298+```bash
185open -W Xcode_9.2.xip
299open -W Xcode_9.2.xip
186sudo cp -R Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS11.2.sdk
300sudo cp -R Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS11.2.sdk
187```
301```
188
302
189
303
190You can verify that you have correct versions by running
304You can verify that you have correct versions by running
191−```
305+```bash
192xcodebuild -showsdks
306xcodebuild -showsdks
193```
307```
194
308
@@ -214,7 +328,7 @@ Ensure that `bundleIdentifier` matches the App ID of the development profile, or
2141. Connect the registered iPhone.
3281. Connect the registered iPhone.
2151. Find your Apple Team ID in the [developer portal](https://developer.apple.com/account/#/membership).
3291. Find your Apple Team ID in the [developer portal](https://developer.apple.com/account/#/membership).
2161. Run the deploy command with your Team ID:
3301. Run the deploy command with your Team ID:
217−```
331+```bash
218result-ios/bin/deploy [TEAM_ID]
332result-ios/bin/deploy [TEAM_ID]
219# or in debug mode via lldb:
333# or in debug mode via lldb:
220result-ios/bin/deploy [TEAM_ID] -d
334result-ios/bin/deploy [TEAM_ID] -d
@@ -224,7 +338,7 @@ result-ios/bin/deploy [TEAM_ID] -d
2241. Go to [developer portal - distribution profiles](https://developer.apple.com/account/ios/profile/production).
3381. Go to [developer portal - distribution profiles](https://developer.apple.com/account/ios/profile/production).
225Create and download a distribution profile.
339Create and download a distribution profile.
2261. Run the package script with your TEAM ID and your distribution profile to create a `.ipa`:
3401. Run the package script with your TEAM ID and your distribution profile to create a `.ipa`:
227−```
341+```bash
228result-ios/bin/package [TEAM_ID] /path/to/output/.ipa /path/to/profile/file
342result-ios/bin/package [TEAM_ID] /path/to/output/.ipa /path/to/profile/file
229```
343```
230
344
@@ -237,7 +351,10 @@ It's also possible to inspect iOS WkWebView apps once they are installed in the
237
351
238### Android
352### Android
239
353
354+NOTE: Currently Android builds are only supported on Linux.
355+
2401. In your project's `default.nix` set a suitable value for `android.applicationId` and `android.displayName`.
3561. In your project's `default.nix` set a suitable value for `android.applicationId` and `android.displayName`.
357+1. In your project's `default.nix` pass `config.android_sdk.accept_license = true;` in the arguments to the import of of `.obelisk/impl` to indicate your acceptance of the [Android Software Development Kit License Agreement](https://developer.android.com/studio/terms), which is required to build Android apps.
2411. Run `nix-build -A android.frontend -o result-android` to build the Android app.
3581. Run `nix-build -A android.frontend -o result-android` to build the Android app.
2421. A debug version of the app should be generated at `result-android/android-app-debug.apk`
3591. A debug version of the app should be generated at `result-android/android-app-debug.apk`
243
360
@@ -254,9 +371,9 @@ This command will accomplish the following:
2541. Build a Signed Android apk for your application
3711. Build a Signed Android apk for your application
2551. Deploy the Signed apk to your connected Android device
3721. Deploy the Signed apk to your connected Android device
256
373
257−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`
374+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`.
258
375
259−Additional documentation on java key stores can be found [here] (https://docs.oracle.com/javase/8/docs/technotes/tools/unix/keytool.html)
376+Additional documentation on Java key stores can be found [here](https://docs.oracle.com/javase/8/docs/technotes/tools/unix/keytool.html).
260
377
261This 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`.
378This 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`.
262
379
all-builds.nixnew file+104
@@ -0,0 +1,104 @@
1+{ self-args ? {
2+ config.android_sdk.accept_license = true;
3+ iosSdkVersion = "10.2";
4+ }
5+, local-self ? import ./. self-args
6+, supportedSystems ? [ builtins.currentSystem ]
7+}:
8+
9+let
10+ inherit (local-self.nixpkgs) lib runCommand nix;
11+ cacheBuildSystems = supportedSystems;
12+
13+ obeliskPackagesCommon = [
14+ "obelisk-frontend"
15+ "obelisk-route"
16+ "obelisk-executable-config-lookup"
17+ ];
18+
19+ obeliskPackagesBackend = obeliskPackagesCommon ++ [
20+ "obelisk-asset-manifest"
21+ "obelisk-asset-serve-snap"
22+ "obelisk-backend"
23+ "obelisk-cliapp"
24+ "obelisk-command"
25+ "obelisk-executable-config-inject"
26+ "obelisk-frontend"
27+ "obelisk-run"
28+ "obelisk-route"
29+ "obelisk-selftest"
30+ "obelisk-snap-extras"
31+ ];
32+
33+ pnameToAttrs = pkgsSet: pnames:
34+ lib.listToAttrs (map
35+ (name: { inherit name; value = pkgsSet.${name}; })
36+ pnames);
37+
38+ collect = v:
39+ if lib.isDerivation v then [v]
40+ else if lib.isAttrs v then lib.concatMap collect (builtins.attrValues v)
41+ else if lib.isList v then lib.concatMap collect v
42+ else [];
43+
44+ perPlatform = lib.genAttrs cacheBuildSystems (system: let
45+ reflex-platform = import ./dep/reflex-platform { inherit system; };
46+
47+ mkPerProfiling = profiling: let
48+ obelisk = import ./. (self-args // { inherit system profiling; });
49+ ghc = pnameToAttrs
50+ obelisk.haskellPackageSets.ghc
51+ obeliskPackagesBackend;
52+ ghcjs = pnameToAttrs
53+ obelisk.haskellPackageSets.ghcjs
54+ obeliskPackagesCommon;
55+ command = obelisk.command;
56+ skeleton = import ./skeleton { inherit obelisk; };
57+ serverSkeletonExe = skeleton.exe;
58+ # TODO fix nixpkgs so it doesn't try to run the result of haskell shells as setup hooks.
59+ serverSkeletonShell = local-self.nixpkgs.runCommand "shell-safe-for-dep" {} ''
60+ touch "$out"
61+ echo "return" >> "$out"
62+ cat "${skeleton.shells.ghc}" >> "$out"
63+ '';
64+ androidSkeleton = (import ./skeleton { inherit obelisk; }).android.frontend;
65+ iosSkeleton = (import ./skeleton { inherit obelisk; }).ios.frontend;
66+ nameSuffix = if profiling then "profiled" else "unprofiled";
67+ packages = {
68+ skeletonProfiledObRun = skeleton.__unstable__.profiledObRun;
69+ inherit
70+ command
71+ serverSkeletonShell
72+ ghc
73+ ;
74+ } // lib.optionalAttrs (!profiling) {
75+ inherit
76+ ghcjs
77+ serverSkeletonExe
78+ ;
79+ } // lib.optionalAttrs reflex-platform.androidSupport {
80+ inherit androidSkeleton;
81+ } // lib.optionalAttrs reflex-platform.iosSupport {
82+ inherit iosSkeleton;
83+ };
84+ in packages // {
85+ cache = reflex-platform.pinBuildInputs
86+ "obelisk-${system}-${nameSuffix}"
87+ (collect packages);
88+ };
89+
90+ perProfiling = {
91+ profiled = mkPerProfiling true;
92+ unprofiled = mkPerProfiling false;
93+ };
94+ in perProfiling // {
95+ cache = reflex-platform.pinBuildInputs
96+ "obelisk-${system}"
97+ (map (p: p.cache) (builtins.attrValues perProfiling));
98+ });
99+
100+ metaCache = local-self.reflex-platform.pinBuildInputs
101+ "obelisk-everywhere"
102+ (map (a: a.cache) (builtins.attrValues perPlatform));
103+
104+in perPlatform // { inherit metaCache; }
all-tests.nixnew file+145
@@ -0,0 +1,145 @@
1+{
2+ supportedSystems ? [ builtins.currentSystem ]
3+}:
4+let
5+ nginxRoot = "/run/nginx";
6+ obelisk = import ./default.nix {};
7+ # Get NixOS a pre-release 20.03 that contains the python based tests and recursive nix
8+ pkgs = import (builtins.fetchTarball https://github.com/nixos/nixpkgs/archive/3de5266.tar.gz) {};
9+ sshKeys = import (pkgs.path + /nixos/tests/ssh-keys.nix) pkgs;
10+ make-test = import (pkgs.path + /nixos/tests/make-test-python.nix);
11+ obelisk-everywhere = (import ./all-builds.nix { inherit supportedSystems; }).x86_64-linux.cache;
12+ snakeOilPrivateKey = sshKeys.snakeOilPrivateKey.text;
13+ snakeOilPublicKey = sshKeys.snakeOilPublicKey;
14+in
15+ make-test ({...}: {
16+ name = "obelisk";
17+ nodes = {
18+ githost = {
19+ networking.firewall.allowedTCPPorts = [ 22 80 ];
20+ services.openssh = {
21+ enable = true;
22+ };
23+ environment.systemPackages = [
24+ pkgs.git
25+ ];
26+ users.users.root.openssh.authorizedKeys.keys = [
27+ snakeOilPublicKey
28+ ];
29+ };
30+
31+ client = {
32+ imports = [
33+ (pkgs.path + /nixos/modules/installer/cd-dvd/channel.nix)
34+ ];
35+ nix.useSandbox = false;
36+ nix.binaryCaches = [];
37+ environment.systemPackages = [
38+ obelisk.command
39+ obelisk.shell
40+ obelisk-everywhere
41+ pkgs.git
42+ ];
43+ };
44+ };
45+
46+ testScript =
47+ let
48+ privateKeyFile = pkgs.writeText "id_rsa" ''${snakeOilPrivateKey}'';
49+ thunkableSample = pkgs.writeText "default.nix" ''
50+ let pkgs = import <nixpkgs> {}; in pkgs.git
51+ '';
52+ invalidThunkableSample = pkgs.writeText "default.nix" ''
53+ let pkgs = import <nixpkgs> {}; in pkgtypo.git
54+ '';
55+ sshConfigFile = pkgs.writeText "ssh_config" ''
56+ Host *
57+ StrictHostKeyChecking no
58+ UserKnownHostsFile=/dev/null
59+ ConnectionAttempts=1
60+ ConnectTimeout=1
61+ IdentityFile=~/.ssh/id_rsa
62+ User=root
63+ '';
64+ in ''
65+ start_all()
66+ githost.wait_for_open_port("22")
67+
68+ with subtest("test obelisk is installed"):
69+ client.succeed("ob --help")
70+
71+ with subtest("test the client can access the server via ssh"):
72+ client.succeed("mkdir -p ~/.ssh/")
73+ client.succeed(
74+ "cp ${privateKeyFile} ~/.ssh/id_rsa"
75+ )
76+ client.succeed("chmod 600 ~/.ssh/id_rsa")
77+ client.wait_until_succeeds(
78+ "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa githost true"
79+ )
80+ client.succeed(
81+ "cp ${sshConfigFile} ~/.ssh/config"
82+ )
83+ client.wait_until_succeeds("ssh githost true")
84+
85+ with subtest("test a remote bare repo can be started"):
86+ githost.succeed("mkdir -p ~/myorg/myapp.git")
87+ githost.succeed("cd ~/myorg/myapp.git && git init --bare")
88+
89+ with subtest("test a git project can be configured with a remote using ssh"):
90+ client.succeed("mkdir -p ~/code/myapp")
91+ client.succeed("cd ~/code/myapp && git init")
92+ client.succeed(
93+ "cp ${thunkableSample} ~/code/myapp/default.nix"
94+ )
95+ client.succeed("cd ~/code/myapp && git add .")
96+ client.succeed('git config --global user.email "you@example.com"')
97+ client.succeed('git config --global user.name "Your Name"')
98+ client.succeed('cd ~/code/myapp && git commit -m "Initial"')
99+ client.succeed(
100+ "cd ~/code/myapp && git remote add origin root@githost:/root/myorg/myapp.git"
101+ )
102+
103+
104+ with subtest("test pushing code to the remote"):
105+ client.succeed("cd ~/code/myapp && git push -u origin master")
106+ client.succeed("cd ~/code/myapp && git status")
107+
108+ with subtest("test obelisk can pack"):
109+ client.succeed("ob -v thunk pack ~/code/myapp")
110+ client.succeed("grep -qF 'git' ~/code/myapp/default.nix")
111+ client.succeed("grep -qF 'myorg' ~/code/myapp/git.json")
112+ client.succeed("ob -v thunk unpack ~/code/myapp")
113+
114+ with subtest("test obelisk can set the public / private flag"):
115+ client.succeed("ob -v thunk pack ~/code/myapp --private")
116+ client.fail("""grep -qF '"private": practice' ~/code/myapp/git.json""")
117+ client.succeed("""grep -qF '"private": true' ~/code/myapp/git.json""")
118+ client.succeed("nix-build ~/code/myapp")
119+ client.succeed("ob -v thunk unpack ~/code/myapp")
120+ client.succeed("ob -v thunk pack ~/code/myapp --public")
121+ client.succeed("""grep -qF '"private": false' ~/code/myapp/git.json""")
122+ client.succeed("nix-build ~/code/myapp")
123+ client.succeed("ob -v thunk unpack ~/code/myapp")
124+
125+ with subtest("test building an invalid thunk fails"):
126+ client.succeed("cd ~/code/myapp && git checkout -b bad")
127+ client.succeed(
128+ "cp ${invalidThunkableSample} ~/code/myapp/default.nix"
129+ )
130+ client.succeed("cd ~/code/myapp && git add .")
131+ client.succeed('git config --global user.email "you@example.com"')
132+ client.succeed('git config --global user.name "Your Name"')
133+ client.succeed('cd ~/code/myapp && git commit -m "Bad commit"')
134+ client.succeed("cd ~/code/myapp && git push -u origin bad")
135+ client.succeed("ob -v thunk pack ~/code/myapp --public")
136+ client.fail("nix-build ~/code/myapp")
137+ client.succeed("ob -v thunk unpack ~/code/myapp")
138+ client.succeed("cd ~/code/myapp && git checkout master")
139+
140+ with subtest("test obelisk can detect private repos"):
141+ client.succeed("ob -v thunk pack ~/code/myapp")
142+ client.succeed("""grep -qF '"private": true' ~/code/myapp/git.json""")
143+ client.succeed("ob -v thunk unpack ~/code/myapp")
144+ '';
145+ }) {}
default.nix+189−108
@@ -1,31 +1,31 @@
1{ system ? builtins.currentSystem
1{ system ? builtins.currentSystem
2, profiling ? false
2, profiling ? false
3, iosSdkVersion ? "10.2"
3, iosSdkVersion ? "10.2"
4−, __useLegacyCompilers ? false
4+, config ? {}
5+, reflex-platform-func ? import ./dep/reflex-platform
5}:
6}:
6let
7let
8+ reflex-platform = getReflexPlatform { inherit system; };
9+ inherit (reflex-platform) hackGet nixpkgs;
10+ pkgs = nixpkgs;
7
11
8− getReflexPlatform = getReflexPlatform' __useLegacyCompilers;
12+ inherit (import dep/gitignore.nix { inherit (nixpkgs) lib; }) gitignoreSource;
9− getReflexPlatform' = __useLegacyCompilers: sys: import ./dep/reflex-platform {
13+
10− inherit iosSdkVersion __useLegacyCompilers;
14+ getReflexPlatform = { system, enableLibraryProfiling ? profiling }: reflex-platform-func {
11− system = sys;
12− enableLibraryProfiling = profiling;
15+ inherit iosSdkVersion config system enableLibraryProfiling;
13
16
14 nixpkgsOverlays = [
17 nixpkgsOverlays = [
15 (import ./nixpkgs-overlays)
18 (import ./nixpkgs-overlays)
16 ];
19 ];
17
20
18 haskellOverlays = [
21 haskellOverlays = [
19− (import ./haskell-overlays/misc-deps.nix)
22+ (import ./haskell-overlays/misc-deps.nix { inherit hackGet; })
23+ pkgs.obeliskExecutableConfig.haskellOverlay
20 (import ./haskell-overlays/obelisk.nix)
24 (import ./haskell-overlays/obelisk.nix)
21 (import ./haskell-overlays/tighten-ob-exes.nix)
25 (import ./haskell-overlays/tighten-ob-exes.nix)
22 ];
26 ];
23 };
27 };
24
28
25− reflex-platform = getReflexPlatform' false system;
26− inherit (reflex-platform) hackGet nixpkgs;
27− pkgs = nixpkgs;
28−
29 # The haskell environment used to build Obelisk itself, e.g. the 'ob' command
29 # The haskell environment used to build Obelisk itself, e.g. the 'ob' command
30 ghcObelisk = reflex-platform.ghc;
30 ghcObelisk = reflex-platform.ghc;
31
31
@@ -43,19 +43,18 @@ in rec {
43 pathGit = ./.; # Used in CI by the migration graph hash algorithm to correctly ignore files.
43 pathGit = ./.; # Used in CI by the migration graph hash algorithm to correctly ignore files.
44 path = reflex-platform.filterGit ./.;
44 path = reflex-platform.filterGit ./.;
45 obelisk = ghcObelisk;
45 obelisk = ghcObelisk;
46− obeliskEnvs = ghcObeliskEnvs;
46+ obeliskEnvs = pkgs.lib.filterAttrs (k: _: pkgs.lib.strings.hasPrefix "obelisk-" k) ghcObeliskEnvs;
47 command = ghcObelisk.obelisk-command;
47 command = ghcObelisk.obelisk-command;
48− shell = pinBuildInputs "obelisk-shell" ([command] ++ command.commandRuntimeDeps) [];
48+ shell = pinBuildInputs "obelisk-shell" ([command] ++ command.commandRuntimeDeps);
49
49
50 selftest = pkgs.writeScript "selftest" ''
50 selftest = pkgs.writeScript "selftest" ''
51− #!/usr/bin/env bash
51+ #!${pkgs.runtimeShell}
52 set -euo pipefail
52 set -euo pipefail
53
53
54 PATH="${command}/bin:$PATH"
54 PATH="${command}/bin:$PATH"
55− export OBELISK_IMPL="${hackGet ./.}"
55+ cd ${./.}
56 "${ghcObelisk.obelisk-selftest}/bin/obelisk-selftest" +RTS -N -RTS "$@"
56 "${ghcObelisk.obelisk-selftest}/bin/obelisk-selftest" +RTS -N -RTS "$@"
57 '';
57 '';
58− #TODO: Why can't I build ./skeleton directly as a derivation? `nix-build -E ./.` doesn't work
59 skeleton = pkgs.runCommand "skeleton" {
58 skeleton = pkgs.runCommand "skeleton" {
60 dir = builtins.filterSource (path: type: builtins.trace path (baseNameOf path != ".obelisk")) ./skeleton;
59 dir = builtins.filterSource (path: type: builtins.trace path (baseNameOf path != ".obelisk")) ./skeleton;
61 } ''
60 } ''
@@ -74,27 +73,42 @@ in rec {
74 obelisk-asset-manifest-generate "$src" "$haskellManifest" ${packageName} ${moduleName} "$symlinked"
73 obelisk-asset-manifest-generate "$src" "$haskellManifest" ${packageName} ${moduleName} "$symlinked"
75 '';
74 '';
76
75
77− compressedJs = frontend: optimizationLevel: pkgs.runCommand "compressedJs" { buildInputs = [ pkgs.closurecompiler ]; } ''
76+ compressedJs = frontend: optimizationLevel: pkgs.runCommand "compressedJs" {} ''
78 mkdir $out
77 mkdir $out
79 cd $out
78 cd $out
79+ # TODO profiling + static shouldn't break and need an ad-hoc workaround like that
80 ln -s "${haskellLib.justStaticExecutables frontend}/bin/frontend.jsexe/all.js" all.unminified.js
80 ln -s "${haskellLib.justStaticExecutables frontend}/bin/frontend.jsexe/all.js" all.unminified.js
81+ ${if optimizationLevel == null then ''
82+ ln -s all.unminified.js all.js
83+ '' else ''
81− 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
84+ ${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
82− echo "//# sourceMappingURL=all.js.map" >> all.js
85+ echo "//# sourceMappingURL=all.js.map" >> all.js
86+ ''}
83 '';
87 '';
84
88
85 serverModules = {
89 serverModules = {
86− mkBaseEc2 = { hostName, routeHost, enableHttps, adminEmail, ... }: {...}: {
90+ mkBaseEc2 = { nixosPkgs, ... }: {...}: {
87 imports = [
91 imports = [
88− (pkgs.path + /nixos/modules/virtualisation/amazon-image.nix)
92+ (nixosPkgs.path + /nixos/modules/virtualisation/amazon-image.nix)
89 ];
93 ];
94+ ec2.hvm = true;
95+ };
96+
97+ mkDefaultNetworking = { adminEmail, enableHttps, hostName, routeHost, ... }: {...}: {
90 networking = {
98 networking = {
91 inherit hostName;
99 inherit hostName;
92 firewall.allowedTCPPorts = if enableHttps then [ 80 443 ] else [ 80 ];
100 firewall.allowedTCPPorts = if enableHttps then [ 80 443 ] else [ 80 ];
93 };
101 };
102+
103+ # `amazon-image.nix` already sets these but if the user provides their own module then
104+ # forgetting these can cause them to lose access to the server!
105+ # https://github.com/NixOS/nixpkgs/blob/fab05f17d15e4e125def4fd4e708d205b41d8d74/nixos/modules/virtualisation/amazon-image.nix#L133-L136
106+ services.openssh.enable = true;
107+ services.openssh.permitRootLogin = "prohibit-password";
108+
94 security.acme.certs = if enableHttps then {
109 security.acme.certs = if enableHttps then {
95 "${routeHost}".email = adminEmail;
110 "${routeHost}".email = adminEmail;
96 } else {};
111 } else {};
97− ec2.hvm = true;
98 };
112 };
99
113
100 mkObeliskApp =
114 mkObeliskApp =
@@ -115,7 +129,8 @@ in rec {
115 enableACME = enableHttps;
129 enableACME = enableHttps;
116 forceSSL = enableHttps;
130 forceSSL = enableHttps;
117 locations.${baseUrl} = {
131 locations.${baseUrl} = {
118− proxyPass = "http://localhost:" + toString internalPort;
132+ proxyPass = "http://127.0.0.1:" + toString internalPort;
133+ proxyWebsockets = true;
119 };
134 };
120 };
135 };
121 };
136 };
@@ -123,10 +138,11 @@ in rec {
123 wantedBy = [ "multi-user.target" ];
138 wantedBy = [ "multi-user.target" ];
124 after = [ "network.target" ];
139 after = [ "network.target" ];
125 restartIfChanged = true;
140 restartIfChanged = true;
141+ path = [ pkgs.gnutar ];
126 script = ''
142 script = ''
127 ln -sft . '${exe}'/*
143 ln -sft . '${exe}'/*
128 mkdir -p log
144 mkdir -p log
129− exec ./backend ${backendArgs} >>backend.out 2>>backend.err </dev/null
145+ exec ./backend ${backendArgs} </dev/null
130 '';
146 '';
131 serviceConfig = {
147 serviceConfig = {
132 User = user;
148 User = user;
@@ -149,119 +165,184 @@ in rec {
149 };
165 };
150 };
166 };
151
167
168+ inherit mkAssets;
169+
152 serverExe = backend: frontend: assets: optimizationLevel: version:
170 serverExe = backend: frontend: assets: optimizationLevel: version:
153 pkgs.runCommand "serverExe" {} ''
171 pkgs.runCommand "serverExe" {} ''
154 mkdir $out
172 mkdir $out
155 set -eux
173 set -eux
156− ln -s "${haskellLib.justStaticExecutables backend}"/bin/* $out/
174+ ln -s "${if profiling then backend else haskellLib.justStaticExecutables backend}"/bin/* $out/
157 ln -s "${mkAssets assets}" $out/static.assets
175 ln -s "${mkAssets assets}" $out/static.assets
158 ln -s ${mkAssets (compressedJs frontend optimizationLevel)} $out/frontend.jsexe.assets
176 ln -s ${mkAssets (compressedJs frontend optimizationLevel)} $out/frontend.jsexe.assets
159 echo ${version} > $out/version
177 echo ${version} > $out/version
160 '';
178 '';
161
179
162− server = { exe, hostName, adminEmail, routeHost, enableHttps, version }@args:
180+ server = { exe, hostName, adminEmail, routeHost, enableHttps, version, module ? serverModules.mkBaseEc2 }@args:
163 let
181 let
164 nixos = import (pkgs.path + /nixos);
182 nixos = import (pkgs.path + /nixos);
165 in nixos {
183 in nixos {
166 system = "x86_64-linux";
184 system = "x86_64-linux";
167 configuration = {
185 configuration = {
168 imports = [
186 imports = [
187+ (module { inherit exe hostName adminEmail routeHost enableHttps version; nixosPkgs = pkgs; })
169− (serverModules.mkBaseEc2 args)
188+ (serverModules.mkDefaultNetworking args)
170 (serverModules.mkObeliskApp args)
189 (serverModules.mkObeliskApp args)
171 ];
190 ];
172 };
191 };
173 };
192 };
174
193
175−
176 # An Obelisk project is a reflex-platform project with a predefined layout and role for each component
194 # An Obelisk project is a reflex-platform project with a predefined layout and role for each component
177− project = base: projectDefinition:
195+ project = base': projectDefinition:
196+ let
178− let projectOut = sys: (getReflexPlatform sys).project (args@{ nixpkgs, ... }:
197+ projectOut = { system, enableLibraryProfiling ? profiling }: let reflexPlatformProject = (getReflexPlatform { inherit system enableLibraryProfiling; }).project; in reflexPlatformProject (args@{ nixpkgs, ... }:
198+ let
199+ inherit (lib.strings) hasPrefix;
200+ mkProject =
179− let mkProject = { android ? null #TODO: Better error when missing
201+ { android ? null #TODO: Better error when missing
180− , ios ? null #TODO: Better error when missing
202+ , ios ? null #TODO: Better error when missing
181− , packages ? {}
203+ , packages ? {}
182− , overrides ? _: _: {}
204+ , overrides ? _: _: {}
183− , staticFiles ? base + /static
205+ , staticFiles ? null
184− , tools ? _: []
206+ , tools ? _: []
185− , shellToolOverrides ? _: _: {}
207+ , shellToolOverrides ? _: _: {}
186− , withHoogle ? false # Setting this to `true` makes shell reloading far slower
208+ , withHoogle ? false # Setting this to `true` makes shell reloading far slower
187− , __closureCompilerOptimizationLevel ? "ADVANCED"
209+ , __closureCompilerOptimizationLevel ? "ADVANCED" # Set this to `null` to skip the closure-compiler step
188− }:
210+ }:
211+ let
212+ allConfig = nixpkgs.lib.makeExtensible (self: {
213+ base = base';
214+ inherit args;
215+ userSettings = {
216+ inherit android ios packages overrides tools shellToolOverrides withHoogle __closureCompilerOptimizationLevel;
217+ staticFiles = if staticFiles == null then self.base + /static else staticFiles;
218+ };
189− let frontendName = "frontend";
219+ frontendName = "frontend";
190− backendName = "backend";
220+ backendName = "backend";
191− commonName = "common";
221+ commonName = "common";
192− staticName = "obelisk-generated-static";
222+ staticName = "obelisk-generated-static";
193− staticFilesImpure = if lib.isDerivation staticFiles then staticFiles else toString staticFiles;
223+ staticFilesImpure = let fs = self.userSettings.staticFiles; in if lib.isDerivation fs then fs else toString fs;
194− processedStatic = processAssets { src = staticFiles; };
224+ processedStatic = processAssets { src = self.userSettings.staticFiles; };
195− # The packages whose names and roles are defined by this package
225+ # The packages whose names and roles are defined by this package
196− predefinedPackages = lib.filterAttrs (_: x: x != null) {
226+ predefinedPackages = lib.filterAttrs (_: x: x != null) {
197− ${frontendName} = nullIfAbsent (base + "/frontend");
227+ ${self.frontendName} = nullIfAbsent (self.base + "/frontend");
198− ${commonName} = nullIfAbsent (base + "/common");
228+ ${self.commonName} = nullIfAbsent (self.base + "/common");
199− ${backendName} = nullIfAbsent (base + "/backend");
229+ ${self.backendName} = nullIfAbsent (self.base + "/backend");
200− };
230+ };
231+ shellPackages = {};
201− combinedPackages = predefinedPackages // packages;
232+ combinedPackages = self.predefinedPackages // self.userSettings.packages // self.shellPackages;
202− projectOverrides = self: super: {
233+ projectOverrides = self': super': {
203− ${staticName} = haskellLib.dontHaddock (self.callCabal2nix staticName processedStatic.haskellManifest {});
234+ ${self.staticName} = haskellLib.dontHaddock (self'.callCabal2nix self.staticName self.processedStatic.haskellManifest {});
204− ${backendName} = haskellLib.addBuildDepend super.${backendName} self.obelisk-run;
235+ ${self.backendName} = haskellLib.addBuildDepend super'.${self.backendName} self'.obelisk-run;
205− };
236+ };
206− totalOverrides = lib.composeExtensions projectOverrides overrides;
237+ totalOverrides = lib.composeExtensions self.projectOverrides self.userSettings.overrides;
207− inherit (lib.strings) hasPrefix;
208− privateConfigDirs = ["config/backend"];
238+ privateConfigDirs = ["config/backend"];
209− injectableConfig = builtins.filterSource (path: _:
239+ injectableConfig = builtins.filterSource (path: _:
210− !(lib.lists.any (x: hasPrefix (toString base + "/" + toString x) (toString path)) privateConfigDirs)
240+ !(lib.lists.any (x: hasPrefix (toString self.base + "/" + toString x) (toString path)) self.privateConfigDirs)
211− );
241+ );
212− __android = configPath: {
242+ __androidWithConfig = configPath: {
213− ${if android == null then null else frontendName} = {
243+ ${if self.userSettings.android == null then null else self.frontendName} = {
214− executableName = "frontend";
244+ executableName = "frontend";
215− ${if builtins.pathExists staticFiles then "assets" else null} =
245+ ${if builtins.pathExists self.userSettings.staticFiles then "assets" else null} =
216− nixpkgs.obeliskExecutableConfig.platforms.android.inject (injectableConfig configPath) processedStatic.symlinked;
246+ nixpkgs.obeliskExecutableConfig.platforms.android.inject
217− } // android;
247+ (self.injectableConfig configPath)
248+ self.processedStatic.symlinked;
249+ } // self.userSettings.android;
250+ };
251+ __iosWithConfig = configPath: {
252+ ${if self.userSettings.ios == null then null else self.frontendName} = {
253+ executableName = "frontend";
254+ ${if builtins.pathExists self.userSettings.staticFiles then "staticSrc" else null} =
255+ nixpkgs.obeliskExecutableConfig.platforms.ios.inject
256+ (self.injectableConfig configPath)
257+ self.processedStatic.symlinked;
258+ } // self.userSettings.ios;
259+ };
260+
261+ shells-ghc = builtins.attrNames (self.predefinedPackages // self.shellPackages);
262+
263+ shells-ghcjs = [
264+ self.frontendName
265+ self.commonName
266+ ];
267+
268+ shells-ghcSavedSplices = [
269+ self.commonName
270+ self.frontendName
271+ ];
272+
273+ project = reflexPlatformProject ({...}: self.projectConfig);
274+ projectConfig = {
275+ inherit (self.userSettings) shellToolOverrides tools withHoogle;
276+ overrides = self.totalOverrides;
277+ packages = self.combinedPackages;
278+ shells = {
279+ ${if self.userSettings.android == null && self.userSettings.ios == null then null else "ghcSavedSplices"} =
280+ lib.filter (x: lib.hasAttr x self.combinedPackages) self.shells-ghcSavedSplices;
281+ ghc = lib.filter (x: lib.hasAttr x self.combinedPackages) self.shells-ghc;
282+ ghcjs = lib.filter (x: lib.hasAttr x self.combinedPackages) self.shells-ghcjs;
218 };
283 };
219− __ios = configPath: {
284+ android = self.__androidWithConfig (self.base + "/config");
220− ${if ios == null then null else frontendName} = {
285+ ios = self.__iosWithConfig (self.base + "/config");
221− executableName = "frontend";
286+
222− ${if builtins.pathExists staticFiles then "staticSrc" else null} =
287+ passthru = {
223− nixpkgs.obeliskExecutableConfig.platforms.ios.inject (injectableConfig configPath) processedStatic.symlinked;
288+ __unstable__.self = allConfig;
224− } // ios;
289+ inherit (self)
290+ staticFilesImpure processedStatic
291+ __iosWithConfig __androidWithConfig
292+ ;
293+ inherit (self.userSettings)
294+ android ios overrides packages shellToolOverrides staticFiles tools withHoogle
295+ __closureCompilerOptimizationLevel
296+ ;
225 };
297 };
226− in {
227− inherit shellToolOverrides tools withHoogle;
228− overrides = totalOverrides;
229− packages = combinedPackages;
230− shells = {
231− ${if android == null && ios == null then null else "ghcSavedSplices"} = (lib.filter (x: lib.hasAttr x combinedPackages) [
232− commonName
233− frontendName
234− ]);
235− ghc = (lib.filter (x: lib.hasAttr x combinedPackages) [
236− backendName
237− commonName
238− frontendName
239− ]);
240− ghcjs = lib.filter (x: lib.hasAttr x combinedPackages) [
241− frontendName
242− commonName
243− ];
244 };
298 };
245− android = __android null;
246− ios = __ios null;
247− passthru = { inherit android ios packages overrides tools shellToolOverrides withHoogle staticFiles staticFilesImpure __closureCompilerOptimizationLevel processedStatic __ios __android; };
248− };
299+ });
300+ in allConfig;
249− in mkProject (projectDefinition args));
301+ in (mkProject (projectDefinition args)).projectConfig);
302+ mainProjectOut = projectOut { inherit system; };
250− serverOn = sys: version: serverExe
303+ serverOn = projectInst: version: serverExe
251− (projectOut sys).ghc.backend
304+ projectInst.ghc.backend
252− (projectOut system).ghcjs.frontend
305+ mainProjectOut.ghcjs.frontend
253− (projectOut sys).passthru.staticFiles
306+ projectInst.passthru.staticFiles
254− (projectOut sys).passthru.__closureCompilerOptimizationLevel
307+ projectInst.passthru.__closureCompilerOptimizationLevel
255 version;
308 version;
256− linuxExe = serverOn "x86_64-linux";
309+ linuxExe = serverOn (projectOut { system = "x86_64-linux"; });
257 dummyVersion = "Version number is only available for deployments";
310 dummyVersion = "Version number is only available for deployments";
311+ in mainProjectOut // {
312+ __unstable__.profiledObRun = let
258− in projectOut system // {
313+ profiled = projectOut { inherit system; enableLibraryProfiling = true; };
314+ exeSource = builtins.toFile "ob-run.hs" ''
315+ module Main where
316+
317+ import Control.Exception
318+ import Reflex.Profiled
319+ import System.Environment
320+
321+ import qualified Obelisk.Run
322+ import qualified Frontend
323+ import qualified Backend
324+
325+ main :: IO ()
326+ main = do
327+ args <- getArgs
328+ let port = read $ args !! 0
329+ assets = args !! 1
330+ profileFile = (args !! 2) <> ".rprof"
331+ Obelisk.Run.run port (Obelisk.Run.runServeAsset assets) Backend.backend Frontend.frontend `finally` writeProfilingData profileFile
332+ '';
333+ in nixpkgs.runCommand "ob-run" {
334+ buildInputs = [ (profiled.ghc.ghcWithPackages (p: [ p.backend p.frontend])) ];
335+ } ''
336+ mkdir -p $out/bin/
337+ ghc -x hs -prof -fno-prof-auto -threaded ${exeSource} -o $out/bin/ob-run
338+ '';
339+
259 linuxExeConfigurable = linuxExe;
340 linuxExeConfigurable = linuxExe;
260 linuxExe = linuxExe dummyVersion;
341 linuxExe = linuxExe dummyVersion;
261− exe = serverOn system dummyVersion;
342+ exe = serverOn mainProjectOut dummyVersion;
262− server = args@{ hostName, adminEmail, routeHost, enableHttps, version }:
343+ server = args@{ hostName, adminEmail, routeHost, enableHttps, version, module ? serverModules.mkBaseEc2 }:
263 server (args // { exe = linuxExe version; });
344 server (args // { exe = linuxExe version; });
264− obelisk = import (base + "/.obelisk/impl") {};
345+ obelisk = import (base' + "/.obelisk/impl") {};
265 };
346 };
266 haskellPackageSets = {
347 haskellPackageSets = {
267 inherit (reflex-platform) ghc ghcjs;
348 inherit (reflex-platform) ghc ghcjs;
dep/.gitignore+1
@@ -1,4 +1,5 @@
1*
1*
2+!*/
2!.gitignore
3!.gitignore
3!*/*.json
4!*/*.json
4!*/default.nix
5!*/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/gitignore.nix/default.nixnew file+8
@@ -0,0 +1,8 @@
1+# DO NOT HAND-EDIT THIS FILE
2+let fetch = { private ? false, fetchSubmodules ? false, owner, repo, rev, sha256, ... }:
3+ if !fetchSubmodules && !private then builtins.fetchTarball {
4+ url = "https://github.com/${owner}/${repo}/archive/${rev}.tar.gz"; inherit sha256;
5+ } else (import <nixpkgs> {}).fetchFromGitHub {
6+ inherit owner repo rev sha256 fetchSubmodules private;
7+ };
8+in import (fetch (builtins.fromJSON (builtins.readFile ./github.json)))
dep/gitignore.nix/github.jsonnew file+8
@@ -0,0 +1,8 @@
1+{
2+ "owner": "hercules-ci",
3+ "repo": "gitignore.nix",
4+ "branch": "master",
5+ "private": false,
6+ "rev": "7415c4feb127845553943a3856cbc5cb967ee5e0",
7+ "sha256": "1zd1ylgkndbb5szji32ivfhwh04mr1sbgrnvbrqpmfb67g2g3r9i"
8+}
dep/hnix/default.nixnew file+7
@@ -0,0 +1,7 @@
1+# DO NOT HAND-EDIT THIS FILE
2+import ((import <nixpkgs> {}).fetchFromGitHub (
3+ let json = builtins.fromJSON (builtins.readFile ./github.json);
4+ in { inherit (json) owner repo rev sha256;
5+ private = json.private or false;
6+ }
7+))
dep/hnix/github.jsonnew file+7
@@ -0,0 +1,7 @@
1+{
2+ "owner": "haskell-nix",
3+ "repo": "hnix",
4+ "branch": "master",
5+ "rev": "6c9c7c310c54372b3db0fdf5a0137b395cde1bdb",
6+ "sha256": "1i5903b7lxqn2s3jarb14h6wdq8bxiik1hp0xy43w5w1hgvvq0g5"
7+}
dep/reflex-platform/default.nix+7−6
@@ -1,7 +1,8 @@
1# DO NOT HAND-EDIT THIS FILE
1# DO NOT HAND-EDIT THIS FILE
2+let fetch = { private ? false, fetchSubmodules ? false, owner, repo, rev, sha256, ... }:
3+ if !fetchSubmodules && !private then builtins.fetchTarball {
4+ url = "https://github.com/${owner}/${repo}/archive/${rev}.tar.gz"; inherit sha256;
2−import ((import <nixpkgs> {}).fetchFromGitHub (
5+ } else (import <nixpkgs> {}).fetchFromGitHub {
6+ inherit owner repo rev sha256 fetchSubmodules private;
7+ };
3− let json = builtins.fromJSON (builtins.readFile ./github.json);
8+in import (fetch (builtins.fromJSON (builtins.readFile ./github.json)))
4− in { inherit (json) owner repo rev sha256;
5− private = json.private or false;
6− }
7−))
dep/reflex-platform/github.json+5−4
@@ -1,7 +1,8 @@
1{
1{
2 "owner": "reflex-frp",
2 "owner": "reflex-frp",
3 "repo": "reflex-platform",
3 "repo": "reflex-platform",
4− "branch": "develop",
4+ "branch": "master",
5− "rev": "a7cd9a23e7faa9c2545a4c111229e28208e42351",
5+ "private": false,
6− "sha256": "0y52rfrhk4zszgprpyni51l0pgq18dg695k5bmpd62c3zxar5mvm"
6+ "rev": "41be4d952b75515a037318aa344dd6b13ad29cfe",
7+ "sha256": "132yqzyzd3c5fjy7wwnwa5d6pjxv5ap2xz0swwbv3h5pwi8jwv0a"
7−}
8+}
dep/snap-core/default.nixnew file+8
@@ -0,0 +1,8 @@
1+# DO NOT HAND-EDIT THIS FILE
2+let fetch = { private ? false, fetchSubmodules ? false, owner, repo, rev, sha256, ... }:
3+ if !fetchSubmodules && !private then builtins.fetchTarball {
4+ url = "https://github.com/${owner}/${repo}/archive/${rev}.tar.gz"; inherit sha256;
5+ } else (import <nixpkgs> {}).fetchFromGitHub {
6+ inherit owner repo rev sha256 fetchSubmodules private;
7+ };
8+in import (fetch (builtins.fromJSON (builtins.readFile ./github.json)))
dep/snap-core/github.jsonnew file+8
@@ -0,0 +1,8 @@
1+{
2+ "owner": "obsidiansystems",
3+ "repo": "snap-core",
4+ "branch": "ts-expose-fileserve-internals",
5+ "private": false,
6+ "rev": "e015fc484e38dd9389c58b277b1884395e553bdf",
7+ "sha256": "1793x1h4lw9vnrii5akhfxx1x9yrgxf3052jf6bs8f1mhaic6mxx"
8+}
guides/app-deploy/README.mdnew file+311
@@ -0,0 +1,311 @@
1+# Obelisk Application Deployment Guide
2+This document serves two purposes, the first one is to guide new users that want to deploy Obelisk/Reflex apps and the second one to provide a walk-through for test driving Obelisk releases.
3+
4+> **Note:** To complete this *entire* guide you need access to a macOS machine running the latest OSX release, a Linux machine (can be a VM on that mac) with Nix installed, an iPhone or iPad, and a recent Android device. It also assumes that you have Nix correctly set up and that your system is set up to fetch from binary caches instead of building everything on your machine. If you notice any of the commands below takes more than 5 minutes, then caches are not likely enabled.
5+>
6+> Refer to the [Obelisk Installation Documentation](https://github.com/obsidiansystems/obelisk#installing-obelisk) for instructions on configuring the binary caches for any Nix-compatible setup.
7+
8+## a) Get Obelisk
9+
10+There are several ways to install Obelisk, but the one we are going to use here allows us to test a single version of Obelisk in different ways. The approach is to clone Obelisk from it's GitHub repository and start a project that uses that local check out.
11+
12+Open a terminal and run the following:
13+
14+```bash
15+export WORKDIR=~/obelisk-guide
16+mkdir -p "$WORKDIR"
17+```
18+
19+Then let's get Obelisk from GitHub using a specific branch:
20+
21+```bash
22+export OBELISK_BRANCH=develop
23+git clone https://github.com/obsidiansystems/obelisk.git -b "$OBELISK_BRANCH" "$WORKDIR/obelisk"
24+```
25+
26+Once you have an Obelisk check out, you can build it using Nix and make a shortcut (alias) for the rest of the terminal session. With this you will be able to type `ob` anywhere as long as you do not close the terminal.
27+
28+```bash
29+alias ob=$(nix-build "$WORKDIR/obelisk" -A command --no-out-link)/bin/ob
30+```
31+
32+In order to start an Obelisk project, we need initialize it in a new directory. Let's call it `myapp`:
33+
34+```bash
35+mkdir -p "$WORKDIR/myapp"
36+cd "$WORKDIR/myapp"
37+ob init --branch "$OBELISK_BRANCH"
38+```
39+
40+> **Note:** If the `--branch` parameter is not used, then Obelisk sets your project up to look at the `master` branch from GitHub. This is quite handy for real life projects but distracting for our test drive since we want to make sure any changes we do to the Obelisk codebase (like testing a pull request) are immediately picked up.
41+
42+## b) Run the Obelisk app on localhost
43+
44+Let's test that we can run a server on localhost. This is quite easy:
45+
46+```bash
47+cd "$WORKDIR/myapp"
48+ob run
49+```
50+
51+Now open a browser and point it to http://localhost:8000 (or just click on this link). You should see the following:
52+
53+![](assets/app-deploy.png)
54+
55+## c) Deploy the web app on a remote machine
56+
57+### Import the NixOS VirtualBox appliance
58+Install [VirtualBox](https://www.virtualbox.org/) on your machine.
59+
60+On NixOS, do this by adding the following line to your `/etc/nixos/configuration.nix` before the last line containing `}`:
61+
62+```nix
63+ virtualisation.virtualbox.host.enable = true;
64+```
65+
66+then `sudo nixos-rebuild switch`.
67+
68+The NixOS [download page](https://nixos.org/nixos/download.html) has a section called "VirtualBox appliances". Download that as the target system. The author used the 19.09 `.ova` file.
69+
70+With the downloaded file, open VirtualBox and import the `.ova` file:
71+
72+![](assets/virtualbox-appliance-import.png)
73+
74+Leave the default settings and finish the import. That will take a few minutes:
75+
76+![](assets/virtualbox-appliance-import-loading.png)
77+
78+You should now see a NixOS machine in the dashboard:
79+
80+![](assets/virtualbox-dashboard-nixos.png)
81+
82+If you are on a network with DHCP on your wireless or network card then select right-click on the "NixOS" image and click "Settings". Go to the "Network" section and make "Attached to:" set to "Bridged Adapter".
83+
84+![](assets/virtualbox-image-settings-network.png)
85+
86+Finally, double-click on the "NixOS" machine and wait for it boot up. If it asks for a password, use `demo`.
87+
88+![](assets/virtualbox-nioxs-after-boot.png)
89+
90+### Configure the virtual machine for SSH
91+
92+Inside the virtual machine open up "Konsole" by clicking on the start menu in the bottom left corner and typing in "Konsole" until you see it as an option in the menu. Then click on it to open it.
93+
94+Change the system configuration using `sudo` to enable SSH access:
95+
96+```bash
97+sudo nano /etc/nixos/configuration.nix
98+```
99+
100+This will ask you to enter your password. Enter `demo`.
101+
102+![](assets/nano-edit-nixos-configuration.png)
103+
104+You need to add the following snippet right before the final line containing `}`.
105+
106+```nix
107+ services.openssh.enable = true;
108+ services.openssh.permitRootLogin = "yes";
109+```
110+
111+Save and close the editor by typing <kbd>Ctrl</kbd>+<kbd>O</kbd>, <kbd>Enter</kbd>, and then <kbd>Ctrl</kbd>+<kbd>X</kbd>.
112+
113+Now run `sudo nixos-rebuild switch` (if it asks for a password, again use `demo`) and then set the root password using `sudo passwd root`. Pick a simple password like `root`. You'll need to use it again.
114+
115+To get the virtual machine's IP address go back to the terminal on your *host* machine (not the virtual machine) and run:
116+
117+```bash
118+export VM_ID=$(VBoxManage list runningvms | grep "NixOS" | awk -F'[{}]' '{print $2}')
119+echo "$VM_ID"
120+```
121+
122+You should see a single identifier that looks like `388bbbe5-e0a2-4b20-9c76-bbaa5746682e`. If you see anything else then you likely you have multiple NixOS machines running. Stop any other machines and run this command again. (Of course, if you're a power user, just change the `grep` to pick out right VM.)
123+
124+Now get the IP address for that machine:
125+
126+```bash
127+export VM_IP=$(VBoxManage guestproperty enumerate "$VM_ID" | grep /V4/IP | awk '{print $4}' | sed 's/,//')
128+echo "$VM_IP"
129+```
130+
131+You should see an IP like `192.168.7.86`.
132+
133+With that you will be able to SSH into that machine from a terminal, using the username `root`.
134+
135+```bash
136+ssh root@$VM_IP echo "Access granted"
137+```
138+
139+This may ask if you want to trust the fingerprint for this server. Answer `yes`. It will ask you for a password. Enter the one you specified above. You should see `Access granted` printed after you type your password and hit <kbd>Enter</kbd>.
140+
141+> **Note:** If instead you see an error like this
142+>
143+> ```
144+> @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
145+> @ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @
146+> @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
147+> IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
148+> Someone could be eavesdropping on you right now (man-in-the-middle attack)!
149+> It is also possible that a host key has just been changed.
150+> ```
151+>
152+> you need to remove the old fingerprint for this IP. Run the following:
153+>
154+> ```bash
155+> sed -i "/^$VM_IP /d" ~/.ssh/known_hosts
156+> ```
157+>
158+> and then try the previous `ssh` command again.
159+
160+Now we'll allow access with an SSH key instead of a password. Create a new SSH key with `ssh-keygen`:
161+
162+```bash
163+ssh-keygen -t ed25519 -f "$WORKDIR/obkey" -P ""
164+```
165+
166+Now copy your local SSH key to the server:
167+
168+```bash
169+ssh-copy-id -i "$WORKDIR/obkey.pub" root@$VM_IP
170+```
171+
172+This will ask you for your password again.
173+
174+Now test login with your key instead:
175+
176+```bash
177+ssh -i "$WORKDIR/obkey" root@$VM_IP echo "Access granted"
178+```
179+
180+This should print `Access granted` *without* asking for a password.
181+
182+### Configure the git remote
183+
184+Before the project can be deployed, it needs to be a valid `git` repository.
185+
186+> **Note:** You need `git` installed and configured. You can do that with:
187+>
188+> ```bash
189+> nix-env -i git
190+> git config --global user.name "John Doe"
191+> git config --global user.email johndoe@example.com
192+> ```
193+
194+```bash
195+git init --bare "$WORKDIR/myapp-git-remote"
196+cd "$WORKDIR/myapp"
197+git init
198+git add --all
199+git commit -m "initial commit"
200+git remote add origin "$WORKDIR/myapp-git-remote"
201+git push -u origin master
202+```
203+
204+### Deploy
205+
206+With that, we can come back to the Obelisk app and initialize the deployment:
207+
208+```bash
209+cd "$WORKDIR/myapp"
210+ob deploy init --ssh-key "$WORKDIR/obkey" --admin-email a@a.a --hostname $VM_IP --route https://$VM_IP "$WORKDIR/myappdeploy" --disable-https
211+```
212+
213+Then configure the deployment for VirtualBox:
214+
215+```bash
216+echo '{nixosPkgs, ...}: {...}: { imports = [ (nixosPkgs.path + /nixos/modules/virtualisation/virtualbox-image.nix) ]; }' > "$WORKDIR/myappdeploy/module.nix"
217+```
218+
219+And deploy:
220+
221+```bash
222+cd "$WORKDIR/myappdeploy"
223+ob deploy push -v
224+```
225+
226+Once that is complete, open the webpage with
227+
228+```bash
229+xdg-open http://$VM_IP
230+```
231+
232+If that fails, just get the URL by running
233+
234+```bash
235+echo http://$VM_IP
236+```
237+
238+and copy/paste that URL into your browser navigation input.
239+
240+It should look like this:
241+
242+![](assets/app-deploy-2.png)
243+
244+Congratulations! You have deployed an Obelisk application to a remote server via SSH.
245+
246+## d) Deploy an Android app (Linux host only)
247+
248+> **Note:** This section requires that you be running a Linux *host*.
249+
250+Modify `$WORKDIR/myapp/default.nix` to set `config.android_sdk.accept_license = true;` and then update your app.
251+
252+```bash
253+cd "$WORKDIR/myapp"
254+sed -i 's/# config.android_sdk.accept_license = false;/config.android_sdk.accept_license = true;/g' default.nix
255+git add default.nix
256+git commit -m"Accept license"
257+git push
258+```
259+
260+Make sure *USB debugging* is enabled in your Android device ([instructions here](https://developer.android.com/studio/debug/dev-options) and connect the device using USB. Be sure to confirm any security prompts on the device.
261+
262+Now update your deployment and deploy to Android:
263+
264+```bash
265+cd "$WORKDIR/myappdeploy"
266+ob deploy update
267+ob deploy test android -v
268+```
269+
270+This deployment will ask you to create a password (at least 6 characters long) and then ask you a series of questions. You can pick arbitrary answers. If the deployment fails, try using different USB ports on your computer and running `ob deploy test android -v` again. The USB cable you use can also make a difference.
271+
272+When connecting your Android device you may be asked to "Allow USB debugging". You need to allow it.
273+![](assets/android-confirm-usb-debugging.jpg)
274+
275+
276+When the deployment is complete opening the app should look something like
277+
278+![](assets/android-app.jpg)
279+
280+Congratulations! You have deployed an Obelisk Android app via USB.
281+
282+## e) Deploy an iOS app (macOS host only)
283+
284+> **Note:** This section requires that you be running a macOS *host*.
285+
286+Verify that you can see the device from XCode and you have installed a Provisioning profile that links your Apple Developer Id and the Device identifier. The workflow depends on whether or not you are an independent developer or part of an organization and is out of scope for this manual.
287+
288+![](assets/xcode-devices.png)
289+
290+Find your `Team ID` at the following URL (Apple Developer Membership details): https://developer.apple.com/account/#/membership/
291+
292+It will be something like `5B445B3WY1`. With that, you can start the deployment workflow after plugging in an iPhone or iPad via USB and setting it to trust the computer:
293+
294+```bash
295+cd "$WORKDIR/myappdeploy"
296+ob deploy test ios <TEAM-ID> # Use your Team ID here
297+```
298+
299+![](assets/ios-deploy-keychain.png)
300+
301+![](assets/ios-deploy-example.png)
302+
303+If there are no errors and the last line says `100% Installed Package` you can open the device and look for the Obelisk app:
304+
305+![](assets/ios-obelisk-app-icon.jpg)
306+
307+![](assets/ios-obelisk-app-open.jpg)
308+
309+Congratulations, you have deployed an Obelisk app on an iOS device.
310+
311+You are now ready to create your own multi-platform application using Obelisk!
guides/app-deploy/assets/android-app.jpgnew file

Binary file not shown.

guides/app-deploy/assets/android-confirm-usb-debugging.jpgnew file

Binary file not shown.

guides/app-deploy/assets/app-deploy-2.pngnew file

Binary file not shown.

guides/app-deploy/assets/app-deploy.pngnew file

Binary file not shown.

guides/app-deploy/assets/ios-deploy-example.pngnew file

Binary file not shown.

guides/app-deploy/assets/ios-deploy-keychain.pngnew file

Binary file not shown.

guides/app-deploy/assets/ios-obelisk-app-icon.jpgnew file

Binary file not shown.

guides/app-deploy/assets/ios-obelisk-app-open.jpgnew file

Binary file not shown.

guides/app-deploy/assets/nano-edit-nixos-configuration.pngnew file

Binary file not shown.

guides/app-deploy/assets/virtualbox-appliance-import-loading.pngnew file

Binary file not shown.

guides/app-deploy/assets/virtualbox-appliance-import.pngnew file

Binary file not shown.

guides/app-deploy/assets/virtualbox-dashboard-nixos.pngnew file

Binary file not shown.

guides/app-deploy/assets/virtualbox-image-settings-network.pngnew file

Binary file not shown.

guides/app-deploy/assets/virtualbox-nioxs-after-boot.pngnew file

Binary file not shown.

guides/app-deploy/assets/xcode-devices.pngnew file

Binary file not shown.

haskell-overlays/misc-deps.nix+11−14
@@ -1,23 +1,20 @@
1+{ hackGet }:
2+
1# Fix misc upstream packages
3# Fix misc upstream packages
2self: super:
4self: super:
3
5
4let
6let
5 pkgs = self.callPackage ({ pkgs }: pkgs) {};
7 pkgs = self.callPackage ({ pkgs }: pkgs) {};
8+ haskellLib = pkgs.haskell.lib;
6in
9in
7
10
8{
11{
9− hnix = pkgs.haskell.lib.dontCheck (self.callCabal2nix "hnix" (pkgs.fetchFromGitHub {
12+ hnix = haskellLib.dontCheck (haskellLib.doJailbreak (self.callCabal2nix "hnix" (hackGet ../dep/hnix) {}));
10− owner = "haskell-nix";
13+ hnix-store-core = self.callHackage "hnix-store-core" "0.1.0.0" {};
11− repo = "hnix";
14+
12− rev = "42afdc21da5d9e076eab57eaa42bfdde938192b8";
15+ ghcid = self.callCabal2nix "ghcid" (hackGet ../dep/ghcid) {};
13− sha256 = "0psw384dx9bw2dp93xrzw8rd9amvcwgzn64jzzwby7sfspj6k349";
16+ # Exports more internals
14− }) {});
17+ snap-core = haskellLib.dontCheck (self.callCabal2nix "snap-core" (hackGet ../dep/snap-core) {});
15− # Need 8.0.2 build support
18+
16− # PR: https://github.com/dmwit/universe/pull/33
19+ shelly = self.callHackage "shelly" "1.9.0" {};
17− universe-template = self.callCabal2nix "universe-template" (pkgs.fetchFromGitHub {
18− owner = "obsidiansystems";
19− repo = "universe";
20− rev = "6a71119bfa5db2b9990a2491c941469ff8ef5d13";
21− sha256 = "0z8smyainnlzcglv3dlx6x1n9j6d2jv48aa8f2421iayfkxg3js5";
22− } + /template) {};
23}
20}
haskell-overlays/obelisk.nix+31−12
@@ -1,23 +1,42 @@
1# Add obelisk packages
1# Add obelisk packages
2+
2self: super:
3self: super:
3
4
4let
5let
5 pkgs = self.callPackage ({ pkgs }: pkgs) {};
6 pkgs = self.callPackage ({ pkgs }: pkgs) {};
6− inherit (pkgs) cleanHaskellSource;
7+ inherit (pkgs) obeliskCleanSource;
8+ haskellLib = pkgs.haskell.lib;
9+ onLinux = pkg: f: if pkgs.stdenv.isLinux then f pkg else pkg;
7in
10in
8
11
9{
12{
10− obelisk-executable-config = pkgs.obeliskExecutableConfig.haskellPackage self;
11 obelisk-executable-config-inject = pkgs.obeliskExecutableConfig.platforms.web.inject self;
13 obelisk-executable-config-inject = pkgs.obeliskExecutableConfig.platforms.web.inject self;
12
14
13− obelisk-asset-manifest = self.callCabal2nix "obelisk-asset-manifest" ../lib/asset/manifest {};
15+ obelisk-asset-manifest = self.callCabal2nix "obelisk-asset-manifest" (obeliskCleanSource ../lib/asset/manifest) {};
14− obelisk-asset-serve-snap = self.callCabal2nix "obelisk-asset-serve-snap" ../lib/asset/serve-snap {};
16+ obelisk-asset-serve-snap = self.callCabal2nix "obelisk-asset-serve-snap" (obeliskCleanSource ../lib/asset/serve-snap) {};
15− obelisk-backend = self.callCabal2nix "obelisk-backend" (cleanHaskellSource ../lib/backend) {};
17+ obelisk-backend = self.callCabal2nix "obelisk-backend" (obeliskCleanSource ../lib/backend) {};
16− obelisk-cliapp = self.callCabal2nix "obelisk-cliapp" (cleanHaskellSource ../lib/cliapp) {};
18+ obelisk-cliapp = self.callCabal2nix "obelisk-cliapp" (obeliskCleanSource ../lib/cliapp) {};
17− obelisk-command = self.callCabal2nix "obelisk-command" (cleanHaskellSource ../lib/command) {};
19+ obelisk-command = haskellLib.overrideCabal (self.callCabal2nix "obelisk-command" (obeliskCleanSource ../lib/command) {}) {
20+ librarySystemDepends = [
21+ pkgs.jre
22+ pkgs.nix
23+ (haskellLib.justStaticExecutables self.ghcid)
24+ ];
25+ };
18− obelisk-frontend = self.callCabal2nix "obelisk-frontend" (cleanHaskellSource ../lib/frontend) {};
26+ obelisk-frontend = self.callCabal2nix "obelisk-frontend" (obeliskCleanSource ../lib/frontend) {};
19− obelisk-run = self.callCabal2nix "obelisk-run" (cleanHaskellSource ../lib/run) {};
27+ obelisk-run = onLinux (self.callCabal2nix "obelisk-run" (obeliskCleanSource ../lib/run) {}) (pkg:
28+ haskellLib.overrideCabal pkg (drv: { librarySystemDepends = [ pkgs.iproute ]; })
29+ );
20− obelisk-route = self.callCabal2nix "obelisk-route" (cleanHaskellSource ../lib/route) {};
30+ obelisk-route = self.callCabal2nix "obelisk-route" (obeliskCleanSource ../lib/route) {};
21− obelisk-selftest = self.callCabal2nix "obelisk-selftest" (cleanHaskellSource ../lib/selftest) {};
31+ obelisk-selftest = haskellLib.overrideCabal (self.callCabal2nix "obelisk-selftest" (obeliskCleanSource ../lib/selftest) {}) {
32+ librarySystemDepends = [
33+ pkgs.cabal-install
34+ pkgs.coreutils
35+ pkgs.git
36+ pkgs.nix
37+ pkgs.rsync
38+ ];
39+ };
22− obelisk-snap-extras = self.callCabal2nix "obelisk-snap-extras" (cleanHaskellSource ../lib/snap-extras) {};
40+ obelisk-snap-extras = self.callCabal2nix "obelisk-snap-extras" (obeliskCleanSource ../lib/snap-extras) {};
41+ tabulation = self.callCabal2nix "tabulation" (obeliskCleanSource ../lib/tabulation) {};
23}
42}
haskell-overlays/tighten-ob-exes.nix+11−30
@@ -3,26 +3,7 @@ self: super:
3let
3let
4 pkgs = self.callPackage ({ pkgs }: pkgs) {};
4 pkgs = self.callPackage ({ pkgs }: pkgs) {};
5 haskellLib = pkgs.haskell.lib;
5 haskellLib = pkgs.haskell.lib;
6− #TODO: Upstream
7− # Modify a Haskell package to add completion scripts for the given
8− # executable produced by it. These completion scripts will be picked up
9− # automatically if the resulting derivation is installed, e.g. by
10− # `nix-env -i`.
11− addOptparseApplicativeCompletionScripts = exeName: pkg: haskellLib.overrideCabal pkg (drv: {
12− postInstall = (drv.postInstall or "") + ''
13− BASH_COMP_DIR="$out/share/bash-completion/completions"
14− mkdir -p "$BASH_COMP_DIR"
15− "$out/bin/${exeName}" --bash-completion-script "$out/bin/${exeName}" >"$BASH_COMP_DIR/ob"
16
6
17− ZSH_COMP_DIR="$out/share/zsh/vendor-completions"
18− mkdir -p "$ZSH_COMP_DIR"
19− "$out/bin/${exeName}" --zsh-completion-script "$out/bin/${exeName}" >"$ZSH_COMP_DIR/_ob"
20−
21− FISH_COMP_DIR="$out/share/fish/vendor_completions.d"
22− mkdir -p "$FISH_COMP_DIR"
23− "$out/bin/${exeName}" --fish-completion-script "$out/bin/${exeName}" >"$FISH_COMP_DIR/ob.fish"
24− '';
25− });
26 commandRuntimeDeps = with pkgs; [
7 commandRuntimeDeps = with pkgs; [
27 coreutils
8 coreutils
28 git
9 git
@@ -35,17 +16,17 @@ in
35 # Dynamic linking with split objects dramatically increases startup time (about
16 # Dynamic linking with split objects dramatically increases startup time (about
36 # 0.5 seconds on a decent machine with SSD), so we do `justStaticExecutables`.
17 # 0.5 seconds on a decent machine with SSD), so we do `justStaticExecutables`.
37 obelisk-command = haskellLib.overrideCabal
18 obelisk-command = haskellLib.overrideCabal
38− (addOptparseApplicativeCompletionScripts "ob"
19+ (haskellLib.generateOptparseApplicativeCompletion "ob"
39− (haskellLib.justStaticExecutables super.obelisk-command))
20+ (haskellLib.justStaticExecutables super.obelisk-command))
40− (drv: {
21+ (drv: {
41− buildTools = (drv.buildTools or []) ++ [ pkgs.buildPackages.makeWrapper ];
22+ buildTools = (drv.buildTools or []) ++ [ pkgs.buildPackages.makeWrapper ];
42− postFixup = ''
23+ postFixup = ''
43− ${drv.postFixup or ""}
24+ ${drv.postFixup or ""}
44− # Make `ob` reference its runtime dependencies.
25+ # Make `ob` reference its runtime dependencies.
45− wrapProgram "$out"/bin/ob --prefix PATH : ${pkgs.lib.makeBinPath commandRuntimeDeps}
26+ wrapProgram "$out"/bin/ob --prefix PATH : ${pkgs.lib.makeBinPath commandRuntimeDeps}
46− '';
27+ '';
47− passthru = { inherit commandRuntimeDeps; };
28+ passthru = { inherit commandRuntimeDeps; };
48− });
29+ });
49
30
50 obelisk-selftest = haskellLib.justStaticExecutables super.obelisk-selftest;
31 obelisk-selftest = haskellLib.justStaticExecutables super.obelisk-selftest;
51}
32}
jobsets.nixdeleted−66
@@ -1,66 +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− };
33− branchJobset = branch: defaults {
34− description = "obelisk-${branch}";
35− inputs = {
36− obelisk = {
37− value = "https://github.com/obsidiansystems/obelisk ${branch}";
38− type = "git";
39− emailresponsible = false;
40− };
41− };
42− };
43− makePr = num: info: {
44− name = "obelisk-pr-${num}";
45− value = defaults {
46− description = "#${num}: ${info.title}";
47− inputs = {
48− obelisk = {
49− #NOTE: This should really use "pull/${num}/merge"; however, GitHub's
50− #status checks only operate on PR heads. This creates a race
51− #condition, which can currently only be solved by requiring PRs to be
52− #up to date before they're merged. See
53− #https://github.com/isaacs/github/issues/1002
54− value = "https://github.com/obsidiansystems/obelisk pull/${num}/head";
55− type = "git";
56− emailresponsible = false;
57− };
58− };
59− };
60− };
61− processedPrs = mapAttrs' makePr (builtins.fromJSON (builtins.readFile prs));
62− jobsetsAttrs = processedPrs //
63− genAttrs ["master"] branchJobset;
64−in {
65− jobsets = pkgs.writeText "spec.json" (builtins.toJSON jobsetsAttrs);
66−}
lib/asset/README.md+10−10
@@ -1,7 +1,7 @@
1## obelisk-asset
1## obelisk-asset
2
2
3−The 'assets.nix' file contains nix expressions that are a mutually
3+The 'assets.nix' file contains nix expressions that are a mutually
4−recursive set of attributes used to create static asset directories
4+recursive set of attributes used to create static asset directories
5with hashable file encodings. This file should be incorporated within
5with hashable file encodings. This file should be incorporated within
6your project's 'default.nix' file. (See ./example/default.nix , line 2)
6your project's 'default.nix' file. (See ./example/default.nix , line 2)
7```
7```
@@ -10,20 +10,20 @@ your project's 'default.nix' file. (See ./example/default.nix , line 2)
10```
10```
11
11
12The 'mkAssets' and 'mkAssetsWith' functions within 'assets.nix' only
12The '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',
14'noEncodings') and a static directory of your choice as arguments.
14'noEncodings') and a static directory of your choice as arguments.
15
15
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
17file inside of it. (See ./example/default.nix, line 11)
17file inside of it. (See ./example/default.nix, line 11)
18```
18```
19−myAssets = assets.mkAssets ./static;
19+myAssets = assets.mkAssets ./static;
20```
20```
21
21
22−Once you have successfully incorporated 'assets.nix' into your
22+Once you have successfully incorporated 'assets.nix' into your
23−project's nix file(s), use nix-build to generate a symlink of
23+project's nix file(s), use nix-build to generate a symlink of
24−your 'mkAssets' expression. The following command from
24+your 'mkAssets' expression. The following command from
25−example/run-example (line 3) will generate an immutable symlink
25+example/run-example (line 3) will generate an immutable symlink
26−that holds hashed static assets.
26+that holds hashed static assets.
27```bash
27```bash
28nix-build -o static.assets -A myAssets
28nix-build -o static.assets -A myAssets
29```
29```
lib/asset/manifest/obelisk-asset-manifest.cabal+1
@@ -17,6 +17,7 @@ library
17 base
17 base
18 , bytestring
18 , bytestring
19 , containers
19 , containers
20+ , deepseq
20 , directory
21 , directory
21 , SHA
22 , SHA
22 , filepath
23 , filepath
lib/asset/manifest/src/Obelisk/Asset/Gather.hs+5−2
@@ -1,10 +1,12 @@
1{-# LANGUAGE OverloadedStrings #-}
1{-# LANGUAGE OverloadedStrings #-}
2+{-# LANGUAGE BangPatterns #-}
2-- | Functions for collecting up files to be processed by the asset pipeline
3-- | Functions for collecting up files to be processed by the asset pipeline
3module Obelisk.Asset.Gather
4module Obelisk.Asset.Gather
4 ( gatherHashedPaths
5 ( gatherHashedPaths
5 , toHashedPath
6 , toHashedPath
6 ) where
7 ) where
7
8
9+import Control.DeepSeq
8import Control.Monad (forM)
10import Control.Monad (forM)
9import Data.Bits
11import Data.Bits
10import qualified Data.ByteString.Lazy as LBS
12import qualified Data.ByteString.Lazy as LBS
@@ -33,7 +35,7 @@ gatherHashedPaths root = go ""
33 let relativePath = subdir </> sub
35 let relativePath = subdir </> sub
34 isFile <- doesFileExist $ root </> relativePath
36 isFile <- doesFileExist $ root </> relativePath
35 if isFile
37 if isFile
36− then do hashedRelativePath <- toHashedPath root relativePath
38+ then do !hashedRelativePath <- force <$> toHashedPath root relativePath
37 return $ Map.singleton relativePath hashedRelativePath
39 return $ Map.singleton relativePath hashedRelativePath
38 else go relativePath
40 else go relativePath
39
41
@@ -47,7 +49,8 @@ toHashedPath root relativePath = do
47 contents <- LBS.readFile path
49 contents <- LBS.readFile path
48 let hashPrefix = T.unpack $ decodeUtf8 $ LBS.toStrict $ toNixBase32 $ bytestringDigest $ sha256 contents
50 let hashPrefix = T.unpack $ decodeUtf8 $ LBS.toStrict $ toNixBase32 $ bytestringDigest $ sha256 contents
49 (dir, filename) = splitFileName relativePath
51 (dir, filename) = splitFileName relativePath
50− return $ normalise $ dir </> (hashPrefix <> "-" <> filename)
52+ !hashedRelativePath = force $ normalise $ dir </> (hashPrefix <> "-" <> filename)
53+ return hashedRelativePath
51
54
52-- | Convert a ByteString to base 32 in the way that Nix does
55-- | Convert a ByteString to base 32 in the way that Nix does
53toNixBase32 :: LBS.ByteString -> LBS.ByteString
56toNixBase32 :: LBS.ByteString -> LBS.ByteString
lib/asset/serve-snap/src/Obelisk/Asset/Accept.hs+6−6
@@ -136,16 +136,16 @@ qvalue = do
136 skipMany lws
136 skipMany lws
137 q0 <|> q1
137 q0 <|> q1
138 where q0 = do
138 where q0 = do
139− char '0'
139+ _ <- char '0'
140 decimals <- option [] $ do
140 decimals <- option [] $ do
141− char '.'
141+ _ <- char '.'
142 starRule (Just 0) (Just numAllowedDigits) digit
142 starRule (Just 0) (Just numAllowedDigits) digit
143 return $ QValue $ MkFixed $ fromIntegral $ (read ('0' : decimals) :: Int) * 10 ^ (numAllowedDigits - length decimals)
143 return $ QValue $ MkFixed $ fromIntegral $ (read ('0' : decimals) :: Int) * 10 ^ (numAllowedDigits - length decimals)
144 q1 = do
144 q1 = do
145− char '1'
145+ _ <- char '1'
146 option () $ do
146 option () $ do
147− char '.'
147+ _ <- char '.'
148− starRule (Just 0) (Just numAllowedDigits) $ char '0'
148+ _ <- starRule (Just 0) (Just numAllowedDigits) $ char '0'
149 return ()
149 return ()
150 return $ QValue 1
150 return $ QValue 1
151 numAllowedDigits :: Int
151 numAllowedDigits :: Int
@@ -232,7 +232,7 @@ hashRule minNum maxNum element = do
232lws :: Parser ()
232lws :: Parser ()
233lws = do
233lws = do
234 option () $ cr >> lf
234 option () $ cr >> lf
235− starRule (Just 1) Nothing $ sp <|> ht
235+ _ <- starRule (Just 1) Nothing $ sp <|> ht
236 return ()
236 return ()
237
237
238-- | See http://www.w3.org/Protocols/rfc2616/rfc2616-sec2.html#sec2.2
238-- | See http://www.w3.org/Protocols/rfc2616/rfc2616-sec2.html#sec2.2
lib/asset/serve-snap/src/Obelisk/Asset/Serve/Snap.hs+21−10
@@ -12,13 +12,15 @@ import Obelisk.Asset.Accept (Encoding (..), acceptEncodingBody, chooseEncoding,
12import Obelisk.Snap.Extras
12import Obelisk.Snap.Extras
13
13
14import Snap
14import Snap
15− (MonadSnap, getHeader, getsRequest, modifyResponse, pass, redirect, sendFile, setContentLength, setContentType, setHeader, setResponseCode)
15+ (MonadSnap, getHeader, getRequest, getsRequest, modifyResponse, pass, redirect, sendFile, setContentLength, setContentType, setHeader, setResponseCode)
16import Snap.Util.FileServe (defaultMimeTypes, fileType, getSafePath, serveFile)
16import Snap.Util.FileServe (defaultMimeTypes, fileType, getSafePath, serveFile)
17+import Snap.Internal.Util.FileServe (checkRangeReq)
17
18
18import Control.Applicative ((<|>))
19import Control.Applicative ((<|>))
19import Control.Exception (handleJust, try, throwIO)
20import Control.Exception (handleJust, try, throwIO)
20−import Control.Monad (forM, liftM)
21+import Control.Monad (forM, liftM, unless)
21import Control.Monad.IO.Class (liftIO)
22import Control.Monad.IO.Class (liftIO)
23+import Control.Monad.Fail (MonadFail)
22import Data.Attoparsec.ByteString (parseOnly, endOfInput)
24import Data.Attoparsec.ByteString (parseOnly, endOfInput)
23import Data.ByteString (ByteString)
25import Data.ByteString (ByteString)
24import qualified Data.ByteString as BS
26import qualified Data.ByteString as BS
@@ -38,7 +40,7 @@ import System.Posix (getFileStatus, fileSize)
38-- current version of the asset
40-- current version of the asset
39-- (e.g. @0rx5yvkkgkig2pcqf4ngi1l7vh89qqajdzc6aiayaibyhvj0d853-test.png@). Hashed
41-- (e.g. @0rx5yvkkgkig2pcqf4ngi1l7vh89qqajdzc6aiayaibyhvj0d853-test.png@). Hashed
40-- asset names will be sent with permanent caching headers.
42-- asset names will be sent with permanent caching headers.
41−serveAssets :: MonadSnap m => FilePath -> FilePath -> m ()
43+serveAssets :: (MonadSnap m, MonadFail m) => FilePath -> FilePath -> m ()
42serveAssets = serveAssets' True
44serveAssets = serveAssets' True
43
45
44-- | Serve static assets from an asset directory generated via @assets.nix@ or, failing that, from a regular directory.
46-- | Serve static assets from an asset directory generated via @assets.nix@ or, failing that, from a regular directory.
@@ -47,15 +49,15 @@ serveAssets = serveAssets' True
47-- browser for the logical asset name (e.g. @test.png@) but instead the current
49-- browser for the logical asset name (e.g. @test.png@) but instead the current
48-- version of the asset with to point the browser at the unique filename for the
50-- version of the asset with to point the browser at the unique filename for the
49-- current version of the asset.
51-- current version of the asset.
50−serveAssetsInPlace :: MonadSnap m => FilePath -> FilePath -> m ()
52+serveAssetsInPlace :: (MonadSnap m, MonadFail m) => FilePath -> FilePath -> m ()
51serveAssetsInPlace = serveAssets' False
53serveAssetsInPlace = serveAssets' False
52
54
53-- | Like 'serveAssets', but only serves a single specified asset
55-- | Like 'serveAssets', but only serves a single specified asset
54−serveAsset :: MonadSnap m => FilePath -> FilePath -> FilePath -> m ()
56+serveAsset :: (MonadSnap m, MonadFail m) => FilePath -> FilePath -> FilePath -> m ()
55serveAsset = serveAsset' True
57serveAsset = serveAsset' True
56
58
57-- | Like 'serveAssetsInPlace', but only serves a single specified asset
59-- | Like 'serveAssetsInPlace', but only serves a single specified asset
58−serveAssetInPlace :: MonadSnap m => FilePath -> FilePath -> FilePath -> m ()
60+serveAssetInPlace :: (MonadSnap m, MonadFail m) => FilePath -> FilePath -> FilePath -> m ()
59serveAssetInPlace = serveAsset' False
61serveAssetInPlace = serveAsset' False
60
62
61-- | Serve static assets from an asset directory generated via @assets.nix@ or, failing that, from a regular directory.
63-- | Serve static assets from an asset directory generated via @assets.nix@ or, failing that, from a regular directory.
@@ -63,13 +65,13 @@ serveAssetInPlace = serveAsset' False
63-- 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
65-- 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
64-- 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
66-- 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
65-- asset doesn't change often. Conversely for @False@, the asset will be served "in place" but made uncacheable.
67-- 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 ()
68+serveAssets' :: (MonadSnap m, MonadFail m) => Bool -> FilePath -> FilePath -> m ()
67serveAssets' doRedirect base fallback = do
69serveAssets' doRedirect base fallback = do
68 pRaw <- getSafePath
70 pRaw <- getSafePath
69 serveAsset' doRedirect base fallback $ if "/" `isSuffixOf` pRaw || pRaw == "" then pRaw <> "index.html" else pRaw
71 serveAsset' doRedirect base fallback $ if "/" `isSuffixOf` pRaw || pRaw == "" then pRaw <> "index.html" else pRaw
70
72
71-- | Serve a single static asset from an asset directory generated via @assets.nix@ or, failing that, from a regular directory.
73-- | 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 ()
74+serveAsset' :: (MonadFail m, MonadSnap m) => Bool -> FilePath -> FilePath -> FilePath -> m ()
73serveAsset' doRedirect base fallback p = do
75serveAsset' doRedirect base fallback p = do
74 assetType <- liftIO $ try $ BS.readFile $ base </> p </> "type"
76 assetType <- liftIO $ try $ BS.readFile $ base </> p </> "type"
75 case assetType of
77 case assetType of
@@ -93,8 +95,17 @@ serveAsset' doRedirect base fallback p = do
93 let finalFilename = base </> p </> "encodings" </> T.unpack (decodeUtf8 e)
95 let finalFilename = base </> p </> "encodings" </> T.unpack (decodeUtf8 e)
94 stat <- liftIO $ getFileStatus finalFilename
96 stat <- liftIO $ getFileStatus finalFilename
95 modifyResponse $ setHeader "Last-Modified" "Thu, 1 Jan 1970 00:00:00 GMT"
97 modifyResponse $ setHeader "Last-Modified" "Thu, 1 Jan 1970 00:00:00 GMT"
98+ . setHeader "Accept-Ranges" "bytes"
96− modifyResponse $ setResponseCode 200 . setContentLength (fromIntegral $ fileSize stat) . setContentType (fileType defaultMimeTypes p)
99+ . setContentType (fileType defaultMimeTypes p)
100+ let size = fromIntegral $ fileSize stat
101+ req <- getRequest
102+ -- Despite the name, this function actually does all of the work for
103+ -- responding to range requests. We only need to handle *none* range
104+ -- requests ourselves.
105+ wasRange <- checkRangeReq req finalFilename size
106+ unless wasRange $ do
107+ modifyResponse $ setResponseCode 200 . setContentLength size
97− sendFile finalFilename
108+ sendFile finalFilename
98 Just _ -> cachePermanently >> modifyResponse (setResponseCode 304)
109 Just _ -> cachePermanently >> modifyResponse (setResponseCode 304)
99 Right "redirect" -> do
110 Right "redirect" -> do
100 mtarget <- liftIO $ getAssetTarget $ base </> p
111 mtarget <- liftIO $ getAssetTarget $ base </> p
lib/backend/obelisk-backend.cabal+3
@@ -8,6 +8,8 @@ library
8 build-depends: base,
8 build-depends: base,
9 bytestring,
9 bytestring,
10 categories,
10 categories,
11+ containers,
12+ cookie,
11 data-default,
13 data-default,
12 dependent-sum,
14 dependent-sum,
13 dependent-sum-template,
15 dependent-sum-template,
@@ -15,6 +17,7 @@ library
15 mtl,
17 mtl,
16 obelisk-asset-serve-snap,
18 obelisk-asset-serve-snap,
17 obelisk-executable-config-inject,
19 obelisk-executable-config-inject,
20+ obelisk-executable-config-lookup,
18 obelisk-frontend,
21 obelisk-frontend,
19 obelisk-route,
22 obelisk-route,
20 obelisk-snap-extras,
23 obelisk-snap-extras,
lib/backend/src/Obelisk/Backend.hs+180−57
@@ -1,3 +1,5 @@
1+{-# LANGUAGE DeriveFunctor #-}
2+{-# LANGUAGE DeriveGeneric #-}
1{-# LANGUAGE FlexibleContexts #-}
3{-# LANGUAGE FlexibleContexts #-}
2{-# LANGUAGE FlexibleInstances #-}
4{-# LANGUAGE FlexibleInstances #-}
3{-# LANGUAGE GADTs #-}
5{-# LANGUAGE GADTs #-}
@@ -6,62 +8,112 @@
6{-# LANGUAGE MultiParamTypeClasses #-}
8{-# LANGUAGE MultiParamTypeClasses #-}
7{-# LANGUAGE OverloadedStrings #-}
9{-# LANGUAGE OverloadedStrings #-}
8{-# LANGUAGE ScopedTypeVariables #-}
10{-# LANGUAGE ScopedTypeVariables #-}
9−{-# LANGUAGE TemplateHaskell #-}
11+{-# OPTIONS_GHC -fno-warn-orphans #-}
10module Obelisk.Backend
12module Obelisk.Backend
11 ( Backend (..)
13 ( Backend (..)
14+ , BackendConfig (..)
15+ , defaultBackendConfig
16+ , StaticAssets (..)
17+ , defaultStaticAssets
18+ -- * Running a backend
19+ , runBackend
20+ , runBackendWith
21+ -- * Configuration of backend
22+ , GhcjsWidgets(..)
23+ , defaultGhcjsWidgets
24+ -- * all.js script loading functions
25+ , deferredGhcjsScript
26+ , delayedGhcjsScript
27+ -- * all.js preload functions
28+ , preloadGhcjs
29+ , renderAllJsPath
12 -- * Re-exports
30 -- * Re-exports
13 , Default (def)
31 , Default (def)
14 , getPageName
32 , getPageName
15 , getRouteWith
33 , getRouteWith
16 , runSnapWithCommandLineArgs
34 , runSnapWithCommandLineArgs
35+ , runSnapWithConfig
17 , serveDefaultObeliskApp
36 , serveDefaultObeliskApp
18 , prettifyOutput
37 , prettifyOutput
19− , runBackend
20 , staticRenderContentType
38 , staticRenderContentType
21− , mkRouteToUrl
39+ , getPublicConfigs
22 ) where
40 ) where
23
41
24−import Prelude hiding (id, (.))
25−import Control.Category
26import Control.Monad
42import Control.Monad
27import Control.Monad.Except
43import Control.Monad.Except
28−import Control.Categorical.Bifunctor
44+import Control.Monad.Fail (MonadFail)
29import Data.ByteString (ByteString)
45import Data.ByteString (ByteString)
30import qualified Data.ByteString.Char8 as BSC8
46import qualified Data.ByteString.Char8 as BSC8
31import Data.Default (Default (..))
47import Data.Default (Default (..))
32import Data.Dependent.Sum
48import Data.Dependent.Sum
33−import Data.Functor.Sum
34import Data.Functor.Identity
49import Data.Functor.Identity
50+import Data.Map (Map)
51+import qualified Data.Map as Map
35import Data.Monoid ((<>))
52import Data.Monoid ((<>))
36import Data.Text (Text)
53import Data.Text (Text)
37import qualified Data.Text as T
54import qualified Data.Text as T
38−import Data.Text.Encoding
55+import GHC.Generics (Generic)
39import Obelisk.Asset.Serve.Snap (serveAsset)
56import Obelisk.Asset.Serve.Snap (serveAsset)
40−import Obelisk.ExecutableConfig.Inject (injectExecutableConfigs)
57+import qualified Obelisk.ExecutableConfig.Lookup as Lookup
41import Obelisk.Frontend
58import Obelisk.Frontend
42import Obelisk.Route
59import Obelisk.Route
43import Obelisk.Snap.Extras (doNotCache, serveFileIfExistsAs)
60import Obelisk.Snap.Extras (doNotCache, serveFileIfExistsAs)
44import Reflex.Dom
61import Reflex.Dom
45import Snap (MonadSnap, Snap, commandLineConfig, defaultConfig, getsRequest, httpServe, modifyResponse
62import Snap (MonadSnap, Snap, commandLineConfig, defaultConfig, getsRequest, httpServe, modifyResponse
46− , rqPathInfo, rqQueryString, setContentType, writeBS, writeText)
63+ , rqPathInfo, rqQueryString, setContentType, writeBS, writeText
64+ , rqCookies, Cookie(..) , setHeader)
47import Snap.Internal.Http.Server.Config (Config (accessLog, errorLog), ConfigLog (ConfigIoLog))
65import Snap.Internal.Http.Server.Config (Config (accessLog, errorLog), ConfigLog (ConfigIoLog))
48import System.IO (BufferMode (..), hSetBuffering, stderr, stdout)
66import System.IO (BufferMode (..), hSetBuffering, stderr, stdout)
49
67
50data Backend backendRoute frontendRoute = Backend
68data Backend backendRoute frontendRoute = Backend
51− { _backend_routeEncoder :: Encoder (Either Text) Identity (R (Sum backendRoute (ObeliskRoute frontendRoute))) PageName
69+ { _backend_routeEncoder :: Encoder (Either Text) Identity (R (FullRoute backendRoute frontendRoute)) PageName
52 , _backend_run :: ((R backendRoute -> Snap ()) -> IO ()) -> IO ()
70 , _backend_run :: ((R backendRoute -> Snap ()) -> IO ()) -> IO ()
53− }
71+ } deriving (Generic)
72+
73+data BackendConfig frontendRoute = BackendConfig
74+ { _backendConfig_runSnap :: !(Snap () -> IO ()) -- ^ Function to run the snap server
75+ , _backendConfig_staticAssets :: !StaticAssets -- ^ Static assets
76+ , _backendConfig_ghcjsWidgets :: !(GhcjsWidgets (Text -> FrontendWidgetT (R frontendRoute) ()))
77+ -- ^ Given the URL of all.js, return the widgets which are responsible for
78+ -- loading the script.
79+ } deriving (Generic)
54
80
55-- | The static assets provided must contain a compiled GHCJS app that corresponds exactly to the Frontend provided
81-- | The static assets provided must contain a compiled GHCJS app that corresponds exactly to the Frontend provided
56data GhcjsApp route = GhcjsApp
82data GhcjsApp route = GhcjsApp
57 { _ghcjsApp_compiled :: !StaticAssets
83 { _ghcjsApp_compiled :: !StaticAssets
58 , _ghcjsApp_value :: !(Frontend route)
84 , _ghcjsApp_value :: !(Frontend route)
85+ } deriving (Generic)
86+
87+-- | Widgets used to load all.js on the frontend
88+data GhcjsWidgets a = GhcjsWidgets
89+ { _ghcjsWidgets_preload :: a
90+ -- ^ A preload widget, placed in the document head
91+ , _ghcjsWidgets_script :: a
92+ -- ^ A script widget, placed in the document body
93+ } deriving (Functor, Generic)
94+
95+
96+-- | Given the URL of all.js, return the widgets which are responsible for
97+-- loading the script. Defaults to 'preloadGhcjs' and 'deferredGhcjsScript'.
98+defaultGhcjsWidgets :: GhcjsWidgets (Text -> FrontendWidgetT r ())
99+defaultGhcjsWidgets = GhcjsWidgets
100+ { _ghcjsWidgets_preload = preloadGhcjs
101+ , _ghcjsWidgets_script = deferredGhcjsScript
59 }
102 }
60
103
61-- | Serve a frontend, which must be the same frontend that Obelisk has built and placed in the default location
104-- | Serve a frontend, which must be the same frontend that Obelisk has built and placed in the default location
62--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
105--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
63−serveDefaultObeliskApp :: MonadSnap m => (R appRoute -> Text) -> ([Text] -> m ()) -> Frontend (R appRoute) -> R (ObeliskRoute appRoute) -> m ()
106+serveDefaultObeliskApp
107+ :: (MonadSnap m, HasCookies m, MonadFail m)
108+ => (R appRoute -> Text)
109+ -> GhcjsWidgets (FrontendWidgetT (R appRoute) ())
110+ -> ([Text] -> m ())
111+ -> Frontend (R appRoute)
112+ -> Map Text ByteString
113+ -> R (ObeliskRoute appRoute)
114+ -> m ()
64−serveDefaultObeliskApp urlEnc serveStaticAsset frontend = serveObeliskApp urlEnc serveStaticAsset frontendApp
115+serveDefaultObeliskApp urlEnc ghcjsWidgets serveStaticAsset frontend =
116+ serveObeliskApp urlEnc ghcjsWidgets serveStaticAsset frontendApp
65 where frontendApp = GhcjsApp
117 where frontendApp = GhcjsApp
66 { _ghcjsApp_compiled = defaultFrontendGhcjsAssets
118 { _ghcjsApp_compiled = defaultFrontendGhcjsAssets
67 , _ghcjsApp_value = frontend
119 , _ghcjsApp_value = frontend
@@ -86,45 +138,56 @@ defaultFrontendGhcjsAssets = StaticAssets
86 , _staticAssets_unprocessed = "frontend.jsexe"
138 , _staticAssets_unprocessed = "frontend.jsexe"
87 }
139 }
88
140
89−runSnapWithCommandLineArgs :: Snap () -> IO ()
141+runSnapWithConfig :: MonadIO m => Config Snap a -> Snap () -> m ()
90−runSnapWithCommandLineArgs a = do
142+runSnapWithConfig conf a = do
91− -- Get the web server configuration from the command line
92− cmdLineConf <- commandLineConfig defaultConfig
93− let httpConf = cmdLineConf
143+ let httpConf = conf
94 { accessLog = Just $ ConfigIoLog BSC8.putStrLn
144 { accessLog = Just $ ConfigIoLog BSC8.putStrLn
95 , errorLog = Just $ ConfigIoLog BSC8.putStrLn
145 , errorLog = Just $ ConfigIoLog BSC8.putStrLn
96 }
146 }
97 -- Start the web server
147 -- Start the web server
98− httpServe httpConf a
148+ liftIO $ httpServe httpConf a
149+
150+-- Get the web server configuration from the command line
151+runSnapWithCommandLineArgs :: MonadIO m => Snap () -> m ()
152+runSnapWithCommandLineArgs s = liftIO (commandLineConfig defaultConfig) >>= \c ->
153+ runSnapWithConfig c s
99
154
100getPageName :: (MonadSnap m) => m PageName
155getPageName :: (MonadSnap m) => m PageName
101getPageName = do
156getPageName = do
102 p <- getsRequest rqPathInfo
157 p <- getsRequest rqPathInfo
103 q <- getsRequest rqQueryString
158 q <- getsRequest rqQueryString
104− let pageNameEncoder' :: Encoder Identity Identity PageName (String, String)
159+ return $ byteStringsToPageName p q
105− pageNameEncoder' = bimap
106− (unpackTextEncoder . pathSegmentsTextEncoder . listToNonEmptyEncoder)
107− (unpackTextEncoder . queryParametersTextEncoder . toListMapEncoder)
108− return $ decode pageNameEncoder' (T.unpack (decodeUtf8 p), T.unpack (decodeUtf8 q))
109
160
110getRouteWith :: (MonadSnap m) => Encoder Identity parse route PageName -> m (parse route)
161getRouteWith :: (MonadSnap m) => Encoder Identity parse route PageName -> m (parse route)
111getRouteWith e = do
162getRouteWith e = do
112 pageName <- getPageName
163 pageName <- getPageName
113 return $ tryDecode e pageName
164 return $ tryDecode e pageName
114
165
166+renderAllJsPath :: Encoder Identity Identity (R (FullRoute a b)) PageName -> Text
167+renderAllJsPath validFullEncoder =
168+ renderObeliskRoute validFullEncoder $ FullRoute_Frontend (ObeliskRoute_Resource ResourceRoute_Ghcjs) :/ ["all.js"]
169+
170+serveObeliskApp
171+ :: (MonadSnap m, HasCookies m, MonadFail m)
172+ => (R appRoute -> Text)
173+ -> GhcjsWidgets (FrontendWidgetT (R appRoute) ())
174+ -> ([Text] -> m ())
175+ -> GhcjsApp (R appRoute)
176+ -> Map Text ByteString
115−serveObeliskApp :: MonadSnap m => (R appRoute -> Text) -> ([Text] -> m ()) -> GhcjsApp (R appRoute) -> R (ObeliskRoute appRoute) -> m ()
177+ -> R (ObeliskRoute appRoute)
178+ -> m ()
116−serveObeliskApp urlEnc serveStaticAsset frontendApp = \case
179+serveObeliskApp urlEnc ghcjsWidgets serveStaticAsset frontendApp config = \case
117− ObeliskRoute_App appRouteComponent :=> Identity appRouteRest -> serveGhcjsApp urlEnc frontendApp $ GhcjsAppRoute_App appRouteComponent :/ appRouteRest
180+ ObeliskRoute_App appRouteComponent :=> Identity appRouteRest -> serveGhcjsApp urlEnc ghcjsWidgets frontendApp config $ GhcjsAppRoute_App appRouteComponent :/ appRouteRest
118 ObeliskRoute_Resource resComponent :=> Identity resRest -> case resComponent :=> Identity resRest of
181 ObeliskRoute_Resource resComponent :=> Identity resRest -> case resComponent :=> Identity resRest of
119 ResourceRoute_Static :=> Identity pathSegments -> serveStaticAsset pathSegments
182 ResourceRoute_Static :=> Identity pathSegments -> serveStaticAsset pathSegments
120− ResourceRoute_Ghcjs :=> Identity pathSegments -> serveGhcjsApp urlEnc frontendApp $ GhcjsAppRoute_Resource :/ pathSegments
183+ ResourceRoute_Ghcjs :=> Identity pathSegments -> serveGhcjsApp urlEnc ghcjsWidgets frontendApp config $ GhcjsAppRoute_Resource :/ pathSegments
121 ResourceRoute_JSaddleWarp :=> Identity _ -> do
184 ResourceRoute_JSaddleWarp :=> Identity _ -> do
122 let msg = "Error: Obelisk.Backend received jsaddle request"
185 let msg = "Error: Obelisk.Backend received jsaddle request"
123 liftIO $ putStrLn $ T.unpack msg
186 liftIO $ putStrLn $ T.unpack msg
124 writeText msg
187 writeText msg
125 ResourceRoute_Version :=> Identity () -> doNotCache >> serveFileIfExistsAs "text/plain" "version"
188 ResourceRoute_Version :=> Identity () -> doNotCache >> serveFileIfExistsAs "text/plain" "version"
126
189
127−serveStaticAssets :: MonadSnap m => StaticAssets -> [Text] -> m ()
190+serveStaticAssets :: (MonadSnap m, MonadFail m) => StaticAssets -> [Text] -> m ()
128serveStaticAssets assets pathSegments = serveAsset (_staticAssets_processed assets) (_staticAssets_unprocessed assets) $ T.unpack $ T.intercalate "/" pathSegments
191serveStaticAssets assets pathSegments = serveAsset (_staticAssets_processed assets) (_staticAssets_unprocessed assets) $ T.unpack $ T.intercalate "/" pathSegments
129
192
130data StaticAssets = StaticAssets
193data StaticAssets = StaticAssets
@@ -141,35 +204,95 @@ staticRenderContentType :: ByteString
141staticRenderContentType = "text/html; charset=utf-8"
204staticRenderContentType = "text/html; charset=utf-8"
142
205
143--TODO: Don't assume we're being served at "/"
206--TODO: Don't assume we're being served at "/"
207+serveGhcjsApp
208+ :: (MonadSnap m, HasCookies m, MonadFail m)
209+ => (R appRouteComponent -> Text)
210+ -> GhcjsWidgets (FrontendWidgetT (R appRouteComponent) ())
211+ -> GhcjsApp (R appRouteComponent)
212+ -> Map Text ByteString
144−serveGhcjsApp :: MonadSnap m => (R appRouteComponent -> Text) -> GhcjsApp (R appRouteComponent) -> R (GhcjsAppRoute appRouteComponent) -> m ()
213+ -> R (GhcjsAppRoute appRouteComponent)
214+ -> m ()
145−serveGhcjsApp urlEnc app = \case
215+serveGhcjsApp urlEnc ghcjsWidgets app config = \case
146 GhcjsAppRoute_App appRouteComponent :=> Identity appRouteRest -> do
216 GhcjsAppRoute_App appRouteComponent :=> Identity appRouteRest -> do
147 modifyResponse $ setContentType staticRenderContentType
217 modifyResponse $ setContentType staticRenderContentType
218+ modifyResponse $ setHeader "Cache-Control" "no-store private"
148− writeBS <=< liftIO $ renderGhcjsFrontend urlEnc (appRouteComponent :/ appRouteRest) $ _ghcjsApp_value app
219+ writeBS <=< renderGhcjsFrontend urlEnc ghcjsWidgets (appRouteComponent :/ appRouteRest) config $ _ghcjsApp_value app
149 GhcjsAppRoute_Resource :=> Identity pathSegments -> serveStaticAssets (_ghcjsApp_compiled app) pathSegments
220 GhcjsAppRoute_Resource :=> Identity pathSegments -> serveStaticAssets (_ghcjsApp_compiled app) pathSegments
150
221
222+-- | Default obelisk backend configuration.
223+defaultBackendConfig :: BackendConfig frontendRoute
224+defaultBackendConfig = BackendConfig runSnapWithCommandLineArgs defaultStaticAssets defaultGhcjsWidgets
225+
226+-- | Run an obelisk backend with the default configuration.
151−runBackend :: Backend fullRoute frontendRoute -> Frontend (R frontendRoute) -> IO ()
227+runBackend :: Backend backendRoute frontendRoute -> Frontend (R frontendRoute) -> IO ()
228+runBackend = runBackendWith defaultBackendConfig
229+
230+-- | Run an obelisk backend with the given configuration.
231+runBackendWith
232+ :: BackendConfig frontendRoute
233+ -> Backend backendRoute frontendRoute
234+ -> Frontend (R frontendRoute)
235+ -> IO ()
152−runBackend backend frontend = case checkEncoder $ _backend_routeEncoder backend of
236+runBackendWith (BackendConfig runSnap staticAssets ghcjsWidgets) backend frontend = case checkEncoder $ _backend_routeEncoder backend of
153 Left e -> fail $ "backend error:\n" <> T.unpack e
237 Left e -> fail $ "backend error:\n" <> T.unpack e
238+ Right validFullEncoder -> do
239+ publicConfigs <- getPublicConfigs
154− Right validFullEncoder -> _backend_run backend $ \serveRoute -> do
240+ _backend_run backend $ \serveRoute ->
155− runSnapWithCommandLineArgs $ do
241+ runSnap $
156− getRouteWith validFullEncoder >>= \case
242+ getRouteWith validFullEncoder >>= \case
157− Identity r -> case r of
243+ Identity r -> case r of
158− InL backendRoute :=> Identity a -> serveRoute $ backendRoute :/ a
244+ FullRoute_Backend backendRoute :/ a -> serveRoute $ backendRoute :/ a
159− InR obeliskRoute :=> Identity a ->
245+ FullRoute_Frontend obeliskRoute :/ a ->
160− serveDefaultObeliskApp (mkRouteToUrl validFullEncoder) (serveStaticAssets defaultStaticAssets) frontend $ obeliskRoute :/ a
246+ serveDefaultObeliskApp routeToUrl (($ allJsUrl) <$> ghcjsWidgets) (serveStaticAssets staticAssets) frontend publicConfigs $
161−
247+ obeliskRoute :/ a
162−mkRouteToUrl :: Encoder Identity parse (R (Sum f (ObeliskRoute r))) PageName -> R r -> Text
248+ where
163−mkRouteToUrl validFullEncoder =
164− let pageNameEncoder' :: Encoder Identity (Either Text) PageName PathQuery = pageNameEncoder
165− in \(k :/ v) -> T.pack . uncurry (<>) . encode pageNameEncoder' . encode validFullEncoder $ (InR $ ObeliskRoute_App k) :/ v
249+ routeToUrl (k :/ v) = renderObeliskRoute validFullEncoder $ FullRoute_Frontend (ObeliskRoute_App k) :/ v
166−
250+ allJsUrl = renderAllJsPath validFullEncoder
167−
251+
168−renderGhcjsFrontend :: MonadIO m => (route -> Text) -> route -> Frontend route -> m ByteString
252+renderGhcjsFrontend
253+ :: (MonadSnap m, HasCookies m)
254+ => (route -> Text)
255+ -> GhcjsWidgets (FrontendWidgetT route ())
256+ -> route
257+ -> Map Text ByteString
258+ -> Frontend route
259+ -> m ByteString
169−renderGhcjsFrontend urlEnc route f = do
260+renderGhcjsFrontend urlEnc ghcjsWidgets route configs f = do
170− let baseTag = elAttr "base" ("href" =: "/") blank --TODO: Figure out the base URL from the routes
261+ cookies <- askCookies
262+ renderFrontendHtml configs cookies urlEnc route f (_ghcjsWidgets_preload ghcjsWidgets) (_ghcjsWidgets_script ghcjsWidgets)
263+
264+-- | Preload all.js in a link tag.
265+-- This is the default preload method.
266+preloadGhcjs :: Text -> FrontendWidgetT r ()
171− ghcjsPreload = elAttr "link" ("rel" =: "preload" <> "as" =: "script" <> "href" =: "ghcjs/all.js") blank
267+preloadGhcjs allJsUrl = elAttr "link" ("rel" =: "preload" <> "as" =: "script" <> "href" =: allJsUrl) blank
268+
269+-- | Load the script from the given URL in a deferred script tag.
270+-- This is the default method.
271+deferredGhcjsScript :: Text -> FrontendWidgetT r ()
172− ghcjsScript = elAttr "script" ("language" =: "javascript" <> "src" =: "ghcjs/all.js" <> "defer" =: "defer") blank
272+deferredGhcjsScript allJsUrl = elAttr "script" ("type" =: "text/javascript" <> "src" =: allJsUrl <> "defer" =: "defer") blank
173− liftIO $ renderFrontendHtml urlEnc route
273+
174− (_frontend_head f >> injectExecutableConfigs >> baseTag >> ghcjsPreload)
274+-- | An all.js script which is loaded after waiting for some time to pass. This
175− (_frontend_body f >> ghcjsScript)
275+-- is useful to ensure any CSS animations on the page can play smoothly before
276+-- blocking the UI thread by running all.js.
277+delayedGhcjsScript
278+ :: Int -- ^ The number of milliseconds to delay loading by
279+ -> Text -- ^ URL to GHCJS app JavaScript
280+ -> FrontendWidgetT r ()
281+delayedGhcjsScript n allJsUrl = elAttr "script" ("type" =: "text/javascript") $ text $ T.unlines
282+ [ "setTimeout(function() {"
283+ , " var all_js_script = document.createElement('script');"
284+ , " all_js_script.type = 'text/javascript';"
285+ , " all_js_script.src = '" <> allJsUrl <> "';"
286+ , " document.body.appendChild(all_js_script);"
287+ , "}, " <> T.pack (show n) <> ");"
288+ ]
289+
290+instance HasCookies Snap where
291+ askCookies = map (\c -> (cookieName c, cookieValue c)) <$> getsRequest rqCookies
292+
293+-- | Get configs from the canonical "public" locations (i.e., locations that obelisk expects to make available
294+-- to frontend applications, and hence visible to end users).
295+getPublicConfigs :: IO (Map Text ByteString)
296+getPublicConfigs = Map.filterWithKey (\k _ -> isMemberOf k ["common", "frontend"]) <$> Lookup.getConfigs
297+ where
298+ isMemberOf k = any (`T.isPrefixOf` k)
lib/cliapp/obelisk-cliapp.cabal+4−2
@@ -7,9 +7,11 @@ cabal-version: >=1.2
7library
7library
8 hs-source-dirs: src
8 hs-source-dirs: src
9 build-depends:
9 build-depends:
10+ aeson
10− ansi-terminal
11+ , ansi-terminal
11 , base
12 , base
12 , bytestring
13 , bytestring
14+ , containers
13 , exceptions
15 , exceptions
14 , io-streams
16 , io-streams
15 , lens
17 , lens
@@ -23,11 +25,11 @@ library
23 , transformers
25 , transformers
24 exposed-modules:
26 exposed-modules:
25 Obelisk.CliApp
27 Obelisk.CliApp
26− Obelisk.CliApp.Demo
27 other-modules:
28 other-modules:
28 Obelisk.CliApp.Logging
29 Obelisk.CliApp.Logging
29 Obelisk.CliApp.Process
30 Obelisk.CliApp.Process
30 Obelisk.CliApp.Spinner
31 Obelisk.CliApp.Spinner
31 Obelisk.CliApp.TerminalString
32 Obelisk.CliApp.TerminalString
33+ Obelisk.CliApp.Theme
32 Obelisk.CliApp.Types
34 Obelisk.CliApp.Types
33 ghc-options: -Wall -fobject-code
35 ghc-options: -Wall -fobject-code
lib/cliapp/src/Obelisk/CliApp.hs+15−6
@@ -33,16 +33,25 @@ module Obelisk.CliApp
33 , Severity (..)
33 , Severity (..)
34
34
35 -- .Process
35 -- .Process
36− , ProcessFailure (..)
37 , AsProcessFailure (..)
36 , AsProcessFailure (..)
38− , readProcessAndLogStderr
37+ , ProcessFailure (..)
39− , readProcessAndLogOutput
38+ , ProcessSpec (..)
40− , readCreateProcessWithExitCode
39+ , callCommand
40+ , callProcess
41 , callProcessAndLogOutput
41 , callProcessAndLogOutput
42 , createProcess_
42 , createProcess_
43− , callProcess
43+ , overCreateProcess
44− , callCommand
44+ , proc
45+ , readCreateProcessWithExitCode
46+ , readProcessAndLogOutput
47+ , readProcessAndLogStderr
48+ , readProcessJSONAndLogStderr
45 , reconstructCommand
49 , reconstructCommand
50+ , setCwd
51+ , setDelegateCtlc
52+ , setEnvOverride
53+ , shell
54+ , waitForProcess
46 ) where
55 ) where
47
56
48import Control.Monad.Log (Severity (..))
57import Control.Monad.Log (Severity (..))
lib/cliapp/src/Obelisk/CliApp/Demo.hsdeleted−52
@@ -1,52 +0,0 @@
1−{-# LANGUAGE FlexibleContexts #-}
2−{-# LANGUAGE OverloadedStrings #-}
3−{-# LANGUAGE ScopedTypeVariables #-}
4−module Obelisk.CliApp.Demo where
5−
6−import Control.Concurrent (threadDelay)
7−import Control.Monad.IO.Class
8−import Data.Semigroup ((<>))
9−import qualified Data.Text as T
10−import System.Process (proc)
11−
12−import Control.Monad.Catch (MonadMask)
13−
14−import Obelisk.CliApp
15−
16−cliDemo
17− :: ( MonadIO m, MonadMask m
18− , CliLog m, HasCliConfig e m, CliThrow e m, AsProcessFailure e, AsUnstructuredError e)
19− => m ()
20−cliDemo = withSpinner "CLI Demo" $ do
21− putLog Notice "This demo will showcase the CLI library functionality"
22− _ <- withSpinner' "Searching long for something" (Just $ \c -> "Discovered " <> T.pack (show c) <> " objects") $ do
23− delay
24− withSpinner "Nested task" $ do
25− delay
26− putLog Notice "In nested task"
27− -- This is a 'no trail' spinner that won't leave a trail (even its sub spinners)
28− withSpinnerNoTrail "In between, doing something temporary" $ do
29− withSpinner "Runnning something deep inside" $ do
30− delay
31− putLog Notice "You can still log from inside no-trail spinners"
32− delay
33− delay
34− putLog Error "Top nested task finished"
35− delay
36− putLog Error "Some user error while spinning"
37− delay
38− putLog Notice "This is some info mesage"
39− putLog Warning "And now a warning as well"
40− delay
41− return (42 :: Integer)
42− putLog Notice "Now we start a 2nd spinner, run a couple of process, the last of which fails:"
43− withSpinner "Looking around" $ do
44− delay
45− output <- readProcessAndLogStderr Notice $ proc "ls" ["-l", "/"]
46− putLog Notice $ "Output was: " <> output
47− delay
48− callProcessAndLogOutput (Notice, Error) $ proc "ls" ["-l", "/does-not-exist"]
49− delay
50− failWith "Something dangerous happened"
51− where
52− delay = liftIO $ threadDelay 1000000
lib/cliapp/src/Obelisk/CliApp/Logging.hs+35−10
@@ -43,13 +43,15 @@ import Data.Semigroup ((<>))
43import Data.Text (Text)
43import Data.Text (Text)
44import qualified Data.Text as T
44import qualified Data.Text as T
45import qualified Data.Text.IO as T
45import qualified Data.Text.IO as T
46+import GHC.IO.Encoding.Types
46import System.Console.ANSI (Color (Red, Yellow), ColorIntensity (Vivid),
47import System.Console.ANSI (Color (Red, Yellow), ColorIntensity (Vivid),
47 ConsoleIntensity (FaintIntensity), ConsoleLayer (Foreground),
48 ConsoleIntensity (FaintIntensity), ConsoleLayer (Foreground),
48 SGR (SetColor, SetConsoleIntensity), clearLine)
49 SGR (SetColor, SetConsoleIntensity), clearLine)
49import System.Exit (ExitCode (..))
50import System.Exit (ExitCode (..))
50−import System.IO (BufferMode (NoBuffering), hFlush, hReady, hSetBuffering, stderr, stdin, stdout)
51+import System.IO
51
52
52import qualified Obelisk.CliApp.TerminalString as TS
53import qualified Obelisk.CliApp.TerminalString as TS
54+import Obelisk.CliApp.Theme
53import Obelisk.CliApp.Types
55import Obelisk.CliApp.Types
54
56
55newCliConfig
57newCliConfig
@@ -63,7 +65,11 @@ newCliConfig sev noColor noSpinner errorLogExitCode = do
63 lock <- newMVar False
65 lock <- newMVar False
64 tipDisplayed <- newIORef False
66 tipDisplayed <- newIORef False
65 stack <- newIORef ([], [])
67 stack <- newIORef ([], [])
68+ textEncoding <- hGetEncoding stdout
69+ let theme = if maybe False supportsUnicode textEncoding
70+ then unicodeTheme
71+ else noUnicodeTheme
66− return $ CliConfig level noColor noSpinner lock tipDisplayed stack errorLogExitCode
72+ return $ CliConfig level noColor noSpinner lock tipDisplayed stack errorLogExitCode theme
67
73
68runCli :: MonadIO m => CliConfig e -> CliT e m a -> m a
74runCli :: MonadIO m => CliConfig e -> CliT e m a -> m a
69runCli c =
75runCli c =
@@ -126,7 +132,7 @@ handleLog' noColor output = do
126 hFlush stdout
132 hFlush stdout
127 Output_Overwrite ts -> liftIO $ do
133 Output_Overwrite ts -> liftIO $ do
128 width <- TS.getTerminalWidth
134 width <- TS.getTerminalWidth
129− T.putStr $ "\r" <> (TS.render (not noColor) width ts)
135+ T.putStr $ "\r" <> TS.render (not noColor) width ts
130 hFlush stdout
136 hFlush stdout
131 Output_ClearLine -> liftIO $ do
137 Output_ClearLine -> liftIO $ do
132 -- Go to the first column and clear the whole line
138 -- Go to the first column and clear the whole line
@@ -172,14 +178,16 @@ withExitFailMessage msg f = f `catch` \(e :: ExitCode) -> do
172
178
173-- | Write log to stdout, with colors (unless `noColor`)
179-- | Write log to stdout, with colors (unless `noColor`)
174writeLog :: (MonadIO m, MonadMask m) => Bool -> Bool -> WithSeverity Text -> m ()
180writeLog :: (MonadIO m, MonadMask m) => Bool -> Bool -> WithSeverity Text -> m ()
175−writeLog withNewLine noColor (WithSeverity severity s)
181+writeLog withNewLine noColor (WithSeverity severity s) = if T.null s then pure () else write
176− | noColor && severity <= Warning = liftIO $ putFn $ T.pack (show severity) <> ": " <> s
177− | not noColor && severity <= Error = TS.putStrWithSGR errorColors h withNewLine s
178− | not noColor && severity <= Warning = TS.putStrWithSGR warningColors h withNewLine s
179− | not noColor && severity >= Debug = TS.putStrWithSGR debugColors h withNewLine s
180− | otherwise = liftIO $ putFn s
181 where
182 where
183+ write
184+ | noColor && severity <= Warning = liftIO $ putFn $ T.pack (show severity) <> ": " <> s
185+ | not noColor && severity <= Error = TS.putStrWithSGR errorColors h withNewLine s
186+ | not noColor && severity <= Warning = TS.putStrWithSGR warningColors h withNewLine s
187+ | not noColor && severity >= Debug = TS.putStrWithSGR debugColors h withNewLine s
188+ | otherwise = liftIO $ putFn s
189+
182− putFn = if withNewLine then (T.hPutStrLn h) else (T.hPutStr h)
190+ putFn = if withNewLine then T.hPutStrLn h else T.hPutStr h
183 h = if severity <= Error then stderr else stdout
191 h = if severity <= Error then stderr else stdout
184 errorColors = [SetColor Foreground Vivid Red]
192 errorColors = [SetColor Foreground Vivid Red]
185 warningColors = [SetColor Foreground Vivid Yellow]
193 warningColors = [SetColor Foreground Vivid Yellow]
@@ -226,3 +234,20 @@ fork :: (HasCliConfig e m, MonadIO m) => CliT e IO () -> m ThreadId
226fork f = do
234fork f = do
227 c <- getCliConfig
235 c <- getCliConfig
228 liftIO $ forkIO $ runCli c f
236 liftIO $ forkIO $ runCli c f
237+
238+-- | Conservatively determines whether the encoding supports Unicode.
239+--
240+-- Currently this uses a whitelist of known-to-work encodings. In principle it
241+-- could test dynamically by opening a file with this encoding, but it doesn't
242+-- look like base exposes any way to determine this in a pure fashion.
243+supportsUnicode :: TextEncoding -> Bool
244+supportsUnicode enc = any ((textEncodingName enc ==) . textEncodingName)
245+ [ utf8
246+ , utf8_bom
247+ , utf16
248+ , utf16be
249+ , utf16le
250+ , utf32
251+ , utf32be
252+ , utf32le
253+ ]
lib/cliapp/src/Obelisk/CliApp/Process.hs+112−43
@@ -9,43 +9,81 @@
9-- | An extension of `System.Process` that integrates with logging (`Obelisk.CLI.Logging`)
9-- | An extension of `System.Process` that integrates with logging (`Obelisk.CLI.Logging`)
10-- and is thus spinner friendly.
10-- and is thus spinner friendly.
11module Obelisk.CliApp.Process
11module Obelisk.CliApp.Process
12+ ( AsProcessFailure (..)
12− ( ProcessFailure (..)
13+ , ProcessFailure (..)
13− , AsProcessFailure (..)
14+ , ProcessSpec (..)
14− , readProcessAndLogStderr
15+ , callCommand
15− , readProcessAndLogOutput
16+ , callProcess
16− , readCreateProcessWithExitCode
17 , callProcessAndLogOutput
17 , callProcessAndLogOutput
18 , createProcess
18 , createProcess
19 , createProcess_
19 , createProcess_
20− , callProcess
20+ , overCreateProcess
21− , callCommand
21+ , proc
22+ , readCreateProcessWithExitCode
23+ , readProcessAndLogOutput
24+ , readProcessAndLogStderr
25+ , readProcessJSONAndLogStderr
22 , reconstructCommand
26 , reconstructCommand
27+ , setCwd
28+ , setDelegateCtlc
29+ , setEnvOverride
30+ , shell
31+ , waitForProcess
23 ) where
32 ) where
24
33
25import Control.Monad ((<=<), join, void)
34import Control.Monad ((<=<), join, void)
26import Control.Monad.Except (throwError)
35import Control.Monad.Except (throwError)
36+import Control.Monad.Fail
27import Control.Monad.IO.Class (MonadIO, liftIO)
37import Control.Monad.IO.Class (MonadIO, liftIO)
28import Control.Lens (Prism', review)
38import Control.Lens (Prism', review)
29import qualified Data.ByteString as BS
39import qualified Data.ByteString as BS
30import qualified Data.ByteString.Char8 as BSC
40import qualified Data.ByteString.Char8 as BSC
31import Data.Function (fix)
41import Data.Function (fix)
42+import Data.Map (Map)
43+import qualified Data.Map as Map
32import Data.Monoid ((<>))
44import Data.Monoid ((<>))
33import Data.Text (Text)
45import Data.Text (Text)
34import qualified Data.Text as T
46import qualified Data.Text as T
35import qualified Data.Text.Encoding as T
47import qualified Data.Text.Encoding as T
48+import Data.Text.Encoding.Error (lenientDecode)
49+import System.Environment (getEnvironment)
36import System.Exit (ExitCode (..))
50import System.Exit (ExitCode (..))
37import System.IO (Handle)
51import System.IO (Handle)
38import System.IO.Streams (InputStream, handleToInputStream)
52import System.IO.Streams (InputStream, handleToInputStream)
39import qualified System.IO.Streams as Streams
53import qualified System.IO.Streams as Streams
40import System.IO.Streams.Concurrent (concurrentMerge)
54import System.IO.Streams.Concurrent (concurrentMerge)
41−import System.Process (CreateProcess, ProcessHandle, StdStream (CreatePipe), cmdspec, std_err, std_out,
55+import System.Process (CreateProcess, ProcessHandle, StdStream (CreatePipe), std_err, std_out)
42− waitForProcess)
43import qualified System.Process as Process
56import qualified System.Process as Process
57+import qualified Data.Aeson as Aeson
44
58
45import Control.Monad.Log (Severity (..))
59import Control.Monad.Log (Severity (..))
46import Obelisk.CliApp.Logging (putLog, putLogRaw)
60import Obelisk.CliApp.Logging (putLog, putLogRaw)
47import Obelisk.CliApp.Types (CliLog, CliThrow)
61import Obelisk.CliApp.Types (CliLog, CliThrow)
48
62
63+data ProcessSpec = ProcessSpec
64+ { _processSpec_createProcess :: !CreateProcess
65+ , _processSpec_overrideEnv :: !(Maybe (Map String String -> Map String String))
66+ }
67+
68+proc :: FilePath -> [String] -> ProcessSpec
69+proc cmd args = ProcessSpec (Process.proc cmd args) Nothing
70+
71+shell :: String -> ProcessSpec
72+shell cmd = ProcessSpec (Process.shell cmd) Nothing
73+
74+setEnvOverride :: (Map String String -> Map String String) -> ProcessSpec -> ProcessSpec
75+setEnvOverride f p = p { _processSpec_overrideEnv = Just f }
76+
77+overCreateProcess :: (CreateProcess -> CreateProcess) -> ProcessSpec -> ProcessSpec
78+overCreateProcess f (ProcessSpec p x) = ProcessSpec (f p) x
79+
80+setDelegateCtlc :: Bool -> ProcessSpec -> ProcessSpec
81+setDelegateCtlc b = overCreateProcess (\p -> p { Process.delegate_ctlc = b })
82+
83+setCwd :: Maybe FilePath -> ProcessSpec -> ProcessSpec
84+setCwd fp = overCreateProcess (\p -> p { Process.cwd = fp })
85+
86+
49-- TODO put back in `Obelisk.CliApp.Process` and use prisms for extensible exceptions
87-- TODO put back in `Obelisk.CliApp.Process` and use prisms for extensible exceptions
50data ProcessFailure = ProcessFailure Process.CmdSpec Int -- exit code
88data ProcessFailure = ProcessFailure Process.CmdSpec Int -- exit code
51 deriving Show
89 deriving Show
@@ -59,18 +97,32 @@ instance AsProcessFailure ProcessFailure where
59 asProcessFailure = id
97 asProcessFailure = id
60
98
61readProcessAndLogStderr
99readProcessAndLogStderr
62− :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e)
100+ :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e, MonadFail m)
63− => Severity -> CreateProcess -> m Text
101+ => Severity -> ProcessSpec -> m Text
64readProcessAndLogStderr sev process = do
102readProcessAndLogStderr sev process = do
65 (out, _err) <- withProcess process $ \_out err -> do
103 (out, _err) <- withProcess process $ \_out err -> do
66 streamToLog =<< liftIO (streamHandle sev err)
104 streamToLog =<< liftIO (streamHandle sev err)
67− liftIO $ T.decodeUtf8 <$> BS.hGetContents out
105+ liftIO $ T.decodeUtf8With lenientDecode <$> BS.hGetContents out
106+
107+readProcessJSONAndLogStderr
108+ :: (Aeson.FromJSON a, MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e, MonadFail m)
109+ => Severity -> ProcessSpec -> m a
110+readProcessJSONAndLogStderr sev process = do
111+ (out, _err) <- withProcess process $ \_out err -> do
112+ streamToLog =<< liftIO (streamHandle sev err)
113+ json <- liftIO $ BS.hGetContents out
114+ case Aeson.eitherDecodeStrict json of
115+ Right a -> pure a
116+ Left err -> do
117+ putLog Error $ "Could not decode process output as JSON: " <> T.pack err
118+ throwError $ review asProcessFailure $ ProcessFailure (Process.cmdspec $ _processSpec_createProcess process) 0
68
119
69readCreateProcessWithExitCode
120readCreateProcessWithExitCode
70 :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e)
121 :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e)
71− => CreateProcess -> m (ExitCode, String, String)
122+ => ProcessSpec -> m (ExitCode, String, String)
72−readCreateProcessWithExitCode process = do
123+readCreateProcessWithExitCode procSpec = do
124+ process <- mkCreateProcess procSpec
73− putLog Debug $ "Creating process: " <> reconstructCommand (cmdspec process)
125+ putLog Debug $ "Creating process: " <> reconstructProcSpec procSpec
74 liftIO $ Process.readCreateProcessWithExitCode process ""
126 liftIO $ Process.readCreateProcessWithExitCode process ""
75
127
76-- | Like `System.Process.readProcess` but logs the combined output (stdout and stderr)
128-- | Like `System.Process.readProcess` but logs the combined output (stdout and stderr)
@@ -81,22 +133,22 @@ readCreateProcessWithExitCode process = do
81-- which case it is advisable to call it with a non-Error severity for stderr, like
133-- which case it is advisable to call it with a non-Error severity for stderr, like
82-- `callProcessAndLogOutput (Debug, Debug)`.
134-- `callProcessAndLogOutput (Debug, Debug)`.
83readProcessAndLogOutput
135readProcessAndLogOutput
84− :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e)
136+ :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e, MonadFail m)
85− => (Severity, Severity) -> CreateProcess -> m Text
137+ => (Severity, Severity) -> ProcessSpec -> m Text
86readProcessAndLogOutput (sev_out, sev_err) process = do
138readProcessAndLogOutput (sev_out, sev_err) process = do
87− (_, Just out, Just err, p) <- createProcess $ process
139+ (_, Just out, Just err, p) <- createProcess $ overCreateProcess
88− { std_out = CreatePipe , std_err = CreatePipe }
140+ (\p -> p { std_out = CreatePipe , std_err = CreatePipe }) process
89
141
90 -- TODO interleave stdout and stderr in log correctly
142 -- TODO interleave stdout and stderr in log correctly
91 streamToLog =<< liftIO (streamHandle sev_err err)
143 streamToLog =<< liftIO (streamHandle sev_err err)
92− outText <- liftIO $ T.decodeUtf8 <$> BS.hGetContents out
144+ outText <- liftIO $ T.decodeUtf8With lenientDecode <$> BS.hGetContents out
93 putLogRaw sev_out outText
145 putLogRaw sev_out outText
94
146
95− liftIO (waitForProcess p) >>= \case
147+ waitForProcess p >>= \case
96 ExitSuccess -> pure outText
148 ExitSuccess -> pure outText
97− ExitFailure code -> throwError $ review asProcessFailure $ ProcessFailure (cmdspec process) code
149+ ExitFailure code -> throwError $ review asProcessFailure $ ProcessFailure (Process.cmdspec $ _processSpec_createProcess process) code
98
150
99−-- | Like `System.Process.callProcess` but logs the combined output (stdout and stderr)
151+-- | Like 'System.Process.callProcess' but logs the combined output (stdout and stderr)
100-- with the corresponding severity.
152-- with the corresponding severity.
101--
153--
102-- Usually this function is called as `callProcessAndLogOutput (Debug, Error)`. However
154-- Usually this function is called as `callProcessAndLogOutput (Debug, Error)`. However
@@ -104,9 +156,8 @@ readProcessAndLogOutput (sev_out, sev_err) process = do
104-- which case it is advisable to call it with a non-Error severity for stderr, like
156-- which case it is advisable to call it with a non-Error severity for stderr, like
105-- `callProcessAndLogOutput (Debug, Debug)`.
157-- `callProcessAndLogOutput (Debug, Debug)`.
106callProcessAndLogOutput
158callProcessAndLogOutput
107−
108− :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e)
159+ :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e, MonadFail m)
109− => (Severity, Severity) -> CreateProcess -> m ()
160+ => (Severity, Severity) -> ProcessSpec -> m ()
110callProcessAndLogOutput (sev_out, sev_err) process =
161callProcessAndLogOutput (sev_out, sev_err) process =
111 void $ withProcess process $ \out err -> do
162 void $ withProcess process $ \out err -> do
112 stream <- liftIO $ join $ combineStream
163 stream <- liftIO $ join $ combineStream
@@ -116,21 +167,31 @@ callProcessAndLogOutput (sev_out, sev_err) process =
116 where
167 where
117 combineStream s1 s2 = concurrentMerge [s1, s2]
168 combineStream s1 s2 = concurrentMerge [s1, s2]
118
169
119−-- | Like `System.Process.createProcess` but also logs (debug) the process being run
170+-- | Like 'System.Process.createProcess' but also logs (debug) the process being run
120createProcess
171createProcess
121 :: (MonadIO m, CliLog m)
172 :: (MonadIO m, CliLog m)
122− => CreateProcess -> m (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)
173+ => ProcessSpec -> m (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)
123−createProcess p = do
174+createProcess procSpec = do
175+ p <- mkCreateProcess procSpec
124− putLog Debug $ "Creating process: " <> reconstructCommand (cmdspec p)
176+ putLog Debug $ "Creating process: " <> reconstructProcSpec procSpec
125 liftIO $ Process.createProcess p
177 liftIO $ Process.createProcess p
126
178
127-- | Like `System.Process.createProcess_` but also logs (debug) the process being run
179-- | Like `System.Process.createProcess_` but also logs (debug) the process being run
128createProcess_
180createProcess_
129 :: (MonadIO m, CliLog m)
181 :: (MonadIO m, CliLog m)
130− => String -> CreateProcess -> m (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)
182+ => String -> ProcessSpec -> m (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)
131−createProcess_ name p = do
183+createProcess_ name procSpec = do
184+ p <- mkCreateProcess procSpec
132− putLog Debug $ "Creating process " <> T.pack name <> ": " <> reconstructCommand (cmdspec p)
185+ putLog Debug $ "Creating process " <> T.pack name <> ": " <> reconstructProcSpec procSpec
133− liftIO $ Process.createProcess p
186+ liftIO $ Process.createProcess_ name p
187+
188+mkCreateProcess :: MonadIO m => ProcessSpec -> m Process.CreateProcess
189+mkCreateProcess (ProcessSpec p override') = do
190+ case override' of
191+ Nothing -> pure p
192+ Just override -> do
193+ procEnv <- Map.fromList <$> maybe (liftIO getEnvironment) pure (Process.env p)
194+ pure $ p { Process.env = Just $ Map.toAscList (override procEnv) }
134
195
135-- | Like `System.Process.callProcess` but also logs (debug) the process being run
196-- | Like `System.Process.callProcess` but also logs (debug) the process being run
136callProcess
197callProcess
@@ -149,18 +210,18 @@ callCommand cmd = do
149 liftIO $ Process.callCommand cmd
210 liftIO $ Process.callCommand cmd
150
211
151withProcess
212withProcess
152− :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e)
213+ :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e, MonadFail m)
153− => CreateProcess -> (Handle -> Handle -> m ()) -> m (Handle, Handle)
214+ => ProcessSpec -> (Handle -> Handle -> m ()) -> m (Handle, Handle)
154withProcess process f = do -- TODO: Use bracket.
215withProcess process f = do -- TODO: Use bracket.
155 -- FIXME: Using `withCreateProcess` here leads to something operating illegally on closed handles.
216 -- FIXME: Using `withCreateProcess` here leads to something operating illegally on closed handles.
156− (_, Just out, Just err, p) <- createProcess $ process
217+ (_, Just out, Just err, p) <- createProcess $ overCreateProcess
157− { std_out = CreatePipe , std_err = CreatePipe }
218+ (\x -> x { std_out = CreatePipe , std_err = CreatePipe }) process
158
219
159 f out err -- Pass the handles to the passed function
220 f out err -- Pass the handles to the passed function
160
221
161− liftIO (waitForProcess p) >>= \case
222+ waitForProcess p >>= \case
162 ExitSuccess -> return (out, err)
223 ExitSuccess -> return (out, err)
163− ExitFailure code -> throwError $ review asProcessFailure $ ProcessFailure (cmdspec process) code
224+ ExitFailure code -> throwError $ review asProcessFailure $ ProcessFailure (Process.cmdspec $ _processSpec_createProcess process) code
164
225
165-- Create an input stream from the file handle, associating each item with the given severity.
226-- Create an input stream from the file handle, associating each item with the given severity.
166streamHandle :: Severity -> Handle -> IO (InputStream (Severity, BSC.ByteString))
227streamHandle :: Severity -> Handle -> IO (InputStream (Severity, BSC.ByteString))
@@ -173,12 +234,20 @@ streamToLog
173streamToLog stream = fix $ \loop -> do
234streamToLog stream = fix $ \loop -> do
174 liftIO (Streams.read stream) >>= \case
235 liftIO (Streams.read stream) >>= \case
175 Nothing -> return ()
236 Nothing -> return ()
176− Just (sev, line) -> putLogRaw sev (T.decodeUtf8 line) >> loop
237+ Just (sev, line) -> putLogRaw sev (T.decodeUtf8With lenientDecode line) >> loop
238+
239+-- | Wrapper around `System.Process.waitForProcess`
240+waitForProcess :: MonadIO m => ProcessHandle -> m ExitCode
241+waitForProcess = liftIO . Process.waitForProcess
177
242
178-- | Pretty print a 'CmdSpec'
243-- | Pretty print a 'CmdSpec'
179reconstructCommand :: Process.CmdSpec -> Text
244reconstructCommand :: Process.CmdSpec -> Text
245+reconstructCommand p = case p of
180−reconstructCommand (Process.ShellCommand str) = T.pack str
246+ Process.ShellCommand str -> T.pack str
181−reconstructCommand (Process.RawCommand c as) = processToShellString c as
247+ Process.RawCommand c as -> processToShellString c as
182 where
248 where
183 processToShellString cmd args = T.unwords $ map quoteAndEscape (cmd : args)
249 processToShellString cmd args = T.unwords $ map quoteAndEscape (cmd : args)
184 quoteAndEscape x = "'" <> T.replace "'" "'\''" (T.pack x) <> "'"
250 quoteAndEscape x = "'" <> T.replace "'" "'\''" (T.pack x) <> "'"
251+
252+reconstructProcSpec :: ProcessSpec -> Text
253+reconstructProcSpec = reconstructCommand . Process.cmdspec . _processSpec_createProcess
lib/cliapp/src/Obelisk/CliApp/Spinner.hs+18−23
@@ -24,6 +24,7 @@ import System.Console.ANSI (Color (Blue, Cyan, Green, Red))
24
24
25import Obelisk.CliApp.Logging (allowUserToMakeLoggingVerbose, fork, putLog)
25import Obelisk.CliApp.Logging (allowUserToMakeLoggingVerbose, fork, putLog)
26import Obelisk.CliApp.TerminalString (TerminalString (..), enquiryCode)
26import Obelisk.CliApp.TerminalString (TerminalString (..), enquiryCode)
27+import Obelisk.CliApp.Theme
27import Obelisk.CliApp.Types (CliLog, CliConfig (..), HasCliConfig, Output (..), getCliConfig)
28import Obelisk.CliApp.Types (CliLog, CliConfig (..), HasCliConfig, Output (..), getCliConfig)
28
29
29-- | Run an action with a CLI spinner.
30-- | Run an action with a CLI spinner.
@@ -51,7 +52,8 @@ withSpinner'
51 -> m a
52 -> m a
52 -> m a
53 -> m a
53withSpinner' msg mkTrail action = do
54withSpinner' msg mkTrail action = do
54− noSpinner <- _cliConfig_noSpinner <$> getCliConfig
55+ cliConf <- getCliConfig
56+ let noSpinner = _cliConfig_noSpinner cliConf
55 if noSpinner
57 if noSpinner
56 then putLog Notice msg >> action
58 then putLog Notice msg >> action
57 else bracket' run cleanup $ const action
59 else bracket' run cleanup $ const action
@@ -61,8 +63,11 @@ withSpinner' msg mkTrail action = do
61 modifyStack pushSpinner >>= \case
63 modifyStack pushSpinner >>= \case
62 True -> do -- Top-level spinner; fork a thread to manage output of anything on the stack
64 True -> do -- Top-level spinner; fork a thread to manage output of anything on the stack
63 ctrleThread <- fork $ allowUserToMakeLoggingVerbose enquiryCode
65 ctrleThread <- fork $ allowUserToMakeLoggingVerbose enquiryCode
66+ cliConf <- getCliConfig
67+ let theme = _cliConfig_theme cliConf
68+ spinner = coloredSpinner $ _cliTheme_spinner theme
64 spinnerThread <- fork $ runSpinner spinner $ \c -> do
69 spinnerThread <- fork $ runSpinner spinner $ \c -> do
65− logs <- renderSpinnerStack c . snd <$> readStack
70+ logs <- renderSpinnerStack theme c . snd <$> readStack
66 logMessage $ Output_Overwrite logs
71 logMessage $ Output_Overwrite logs
67 pure [ctrleThread, spinnerThread]
72 pure [ctrleThread, spinnerThread]
68 False -> -- Sub-spinner; nothing to do.
73 False -> -- Sub-spinner; nothing to do.
@@ -70,13 +75,15 @@ withSpinner' msg mkTrail action = do
70 cleanup tids resultM = do
75 cleanup tids resultM = do
71 liftIO $ mapM_ killThread tids
76 liftIO $ mapM_ killThread tids
72 logMessage Output_ClearLine
77 logMessage Output_ClearLine
78+ cliConf <- getCliConfig
79+ let theme = _cliConfig_theme cliConf
73− logsM <- modifyStack $ popSpinner $ case resultM of
80+ logsM <- modifyStack $ (popSpinner theme) $ case resultM of
74 Nothing ->
81 Nothing ->
75− ( TerminalString_Colorized Red "✖"
82+ ( TerminalString_Colorized Red $ _cliTheme_failed $ _cliConfig_theme cliConf
76 , Just msg -- Always display final message if there was an exception.
83 , Just msg -- Always display final message if there was an exception.
77 )
84 )
78 Just result ->
85 Just result ->
79− ( TerminalString_Colorized Green "✔"
86+ ( TerminalString_Colorized Green $ _cliTheme_done $ _cliConfig_theme cliConf
80 , mkTrail <*> pure result
87 , mkTrail <*> pure result
81 )
88 )
82 -- Last message, finish off with newline.
89 -- Last message, finish off with newline.
@@ -87,10 +94,10 @@ withSpinner' msg mkTrail action = do
87 )
94 )
88 where
95 where
89 isTemporary = isNothing mkTrail
96 isTemporary = isNothing mkTrail
90− popSpinner (mark, trailMsgM) (flag, old) =
97+ popSpinner theme (mark, trailMsgM) (flag, old) =
91 ( (newFlag, new)
98 ( (newFlag, new)
92 -- With final trail spinner message to render
99 -- With final trail spinner message to render
93− , renderSpinnerStack mark . (: new) . TerminalString_Normal <$> (
100+ , renderSpinnerStack theme mark . (: new) . TerminalString_Normal <$> (
94 if inTemporarySpinner then Nothing else trailMsgM
101 if inTemporarySpinner then Nothing else trailMsgM
95 )
102 )
96 )
103 )
@@ -102,19 +109,19 @@ withSpinner' msg mkTrail action = do
102 =<< fmap _cliConfig_spinnerStack getCliConfig
109 =<< fmap _cliConfig_spinnerStack getCliConfig
103 modifyStack f = liftIO . flip atomicModifyIORef' f
110 modifyStack f = liftIO . flip atomicModifyIORef' f
104 =<< fmap _cliConfig_spinnerStack getCliConfig
111 =<< fmap _cliConfig_spinnerStack getCliConfig
105− spinner = coloredSpinner defaultSpinnerTheme
106
112
107-- | How nested spinner logs should be displayed
113-- | How nested spinner logs should be displayed
108renderSpinnerStack
114renderSpinnerStack
115+ :: CliTheme
109− :: TerminalString -- ^ That which comes before the final element in stack
116+ -> TerminalString -- ^ That which comes before the final element in stack
110 -> [TerminalString] -- ^ Spinner elements in reverse order
117 -> [TerminalString] -- ^ Spinner elements in reverse order
111 -> [TerminalString]
118 -> [TerminalString]
112−renderSpinnerStack mark = L.intersperse space . go . L.reverse
119+renderSpinnerStack theme mark = L.intersperse space . go . L.reverse
113 where
120 where
114 go [] = []
121 go [] = []
115 go (x:[]) = mark : [x]
122 go (x:[]) = mark : [x]
116 go (x:xs) = arrow : x : go xs
123 go (x:xs) = arrow : x : go xs
117− arrow = TerminalString_Colorized Blue "⇾"
124+ arrow = TerminalString_Colorized Blue $ _cliTheme_arrow theme
118 space = TerminalString_Normal " "
125 space = TerminalString_Normal " "
119
126
120-- | A spinner is simply an infinite list of strings that supplant each other in a delayed loop, creating the
127-- | A spinner is simply an infinite list of strings that supplant each other in a delayed loop, creating the
@@ -130,18 +137,6 @@ runSpinner spinner f = forM_ spinner $ f >=> const delay
130 where
137 where
131 delay = liftIO $ threadDelay 100000 -- A shorter delay ensures that we update promptly.
138 delay = liftIO $ threadDelay 100000 -- A shorter delay ensures that we update promptly.
132
139
133−type SpinnerTheme = [Text]
134−
135−-- Find more spinners at https://github.com/sindresorhus/cli-spinners/blob/master/spinners.json
136−_spinnerCircleHalves :: SpinnerTheme
137−_spinnerCircleHalves = ["◐", "◓", "◑", "◒"]
138−
139−spinnerMoon :: SpinnerTheme
140−spinnerMoon = ["🌑", "🌒", "🌓", "🌔", "🌕", "🌖", "🌗", "🌘"]
141−
142−defaultSpinnerTheme :: SpinnerTheme
143−defaultSpinnerTheme = spinnerMoon
144−
145-- | Like `bracket` but the `release` function can know whether an exception was raised
140-- | Like `bracket` but the `release` function can know whether an exception was raised
146bracket' :: MonadMask m => m a -> (a -> Maybe c -> m b) -> (a -> m c) -> m c
141bracket' :: MonadMask m => m a -> (a -> Maybe c -> m b) -> (a -> m c) -> m c
147bracket' acquire release use = mask $ \unmasked -> do
142bracket' acquire release use = mask $ \unmasked -> do
lib/cliapp/src/Obelisk/CliApp/Theme.hsnew file+29
@@ -0,0 +1,29 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+module Obelisk.CliApp.Theme where
3+
4+import Data.Text (Text)
5+
6+data CliTheme = CliTheme
7+ { _cliTheme_done :: Text
8+ , _cliTheme_failed :: Text
9+ , _cliTheme_arrow :: Text
10+ , _cliTheme_spinner :: SpinnerTheme
11+ }
12+
13+type SpinnerTheme = [Text]
14+
15+unicodeTheme :: CliTheme
16+unicodeTheme = CliTheme
17+ { _cliTheme_done = "✔"
18+ , _cliTheme_failed = "✖"
19+ , _cliTheme_arrow = "⇾"
20+ , _cliTheme_spinner = ["◐", "◓", "◑", "◒"]
21+ }
22+
23+noUnicodeTheme :: CliTheme
24+noUnicodeTheme = CliTheme
25+ { _cliTheme_done = "DONE"
26+ , _cliTheme_failed = "FAILED"
27+ , _cliTheme_arrow = "->"
28+ , _cliTheme_spinner = ["|", "/", "-", "\\"]
29+ }
lib/cliapp/src/Obelisk/CliApp/Types.hs+10−3
@@ -6,12 +6,14 @@
6{-# LANGUAGE GeneralizedNewtypeDeriving #-}
6{-# LANGUAGE GeneralizedNewtypeDeriving #-}
7{-# LANGUAGE MultiParamTypeClasses #-}
7{-# LANGUAGE MultiParamTypeClasses #-}
8{-# LANGUAGE OverloadedStrings #-}
8{-# LANGUAGE OverloadedStrings #-}
9+{-# LANGUAGE StandaloneDeriving #-}
9{-# LANGUAGE UndecidableInstances #-}
10{-# LANGUAGE UndecidableInstances #-}
10module Obelisk.CliApp.Types where
11module Obelisk.CliApp.Types where
11
12
12import Control.Concurrent.MVar (MVar)
13import Control.Concurrent.MVar (MVar)
13import Control.Monad.Catch (MonadCatch, MonadMask, MonadThrow)
14import Control.Monad.Catch (MonadCatch, MonadMask, MonadThrow)
15+import Control.Monad.Fail (MonadFail)
14−import Control.Monad.Log (LoggingT, MonadLog, Severity (..), WithSeverity (..), logMessage)
16+import Control.Monad.Log (LoggingT(..), MonadLog, Severity (..), WithSeverity (..), logMessage)
15import Control.Monad.Reader (MonadIO, ReaderT (..), MonadReader (..), ask, mapReaderT)
17import Control.Monad.Reader (MonadIO, ReaderT (..), MonadReader (..), ask, mapReaderT)
16import Control.Monad.Writer (WriterT)
18import Control.Monad.Writer (WriterT)
17import Control.Monad.State (StateT)
19import Control.Monad.State (StateT)
@@ -23,6 +25,7 @@ import Data.Text (Text)
23import System.Exit (ExitCode (..), exitWith)
25import System.Exit (ExitCode (..), exitWith)
24
26
25import Obelisk.CliApp.TerminalString (TerminalString)
27import Obelisk.CliApp.TerminalString (TerminalString)
28+import Obelisk.CliApp.Theme (CliTheme)
26
29
27--------------------------------------------------------------------------------
30--------------------------------------------------------------------------------
28
31
@@ -44,9 +47,11 @@ type CliThrow e m = MonadError e m
44putLog :: CliLog m => Severity -> Text -> m ()
47putLog :: CliLog m => Severity -> Text -> m ()
45putLog sev = logMessage . Output_Log . WithSeverity sev
48putLog sev = logMessage . Output_Log . WithSeverity sev
46
49
50+deriving instance MonadFail m => MonadFail (LoggingT Output m)
51+
47newtype DieT e m a = DieT { unDieT :: ReaderT (e -> (Text, Int)) (LoggingT Output m) a }
52newtype DieT e m a = DieT { unDieT :: ReaderT (e -> (Text, Int)) (LoggingT Output m) a }
48 deriving
53 deriving
49− ( Functor, Applicative, Monad, MonadIO
54+ ( Functor, Applicative, Monad, MonadIO, MonadFail
50 , MonadThrow, MonadCatch, MonadMask
55 , MonadThrow, MonadCatch, MonadMask
51 , MonadLog Output
56 , MonadLog Output
52 )
57 )
@@ -88,6 +93,8 @@ data CliConfig e = CliConfig
88 _cliConfig_spinnerStack :: IORef ([Bool], [TerminalString])
93 _cliConfig_spinnerStack :: IORef ([Bool], [TerminalString])
89 , -- | Failure handler. How to log error and what exit status to use.
94 , -- | Failure handler. How to log error and what exit status to use.
90 _cliConfig_errorLogExitCode :: e -> (Text, Int)
95 _cliConfig_errorLogExitCode :: e -> (Text, Int)
96+ , -- | Theme strings for spinners
97+ _cliConfig_theme :: CliTheme
91 }
98 }
92
99
93class Monad m => HasCliConfig e m | m -> e where
100class Monad m => HasCliConfig e m | m -> e where
@@ -111,7 +118,7 @@ newtype CliT e m a = CliT
111 { unCliT :: ReaderT (CliConfig e) (DieT e m) a
118 { unCliT :: ReaderT (CliConfig e) (DieT e m) a
112 }
119 }
113 deriving
120 deriving
114− ( Functor, Applicative, Monad, MonadIO
121+ ( Functor, Applicative, Monad, MonadIO, MonadFail
115 , MonadThrow, MonadCatch, MonadMask
122 , MonadThrow, MonadCatch, MonadMask
116 , MonadLog Output -- CliLog
123 , MonadLog Output -- CliLog
117 , MonadError e -- CliThrow
124 , MonadError e -- CliThrow
lib/command/obelisk-command.cabal+11−12
@@ -1,5 +1,5 @@
1name: obelisk-command
1name: obelisk-command
2−version: 0.1
2+version: 0.7.0.1
3cabal-version: >= 1.8
3cabal-version: >= 1.8
4build-type: Simple
4build-type: Simple
5
5
@@ -7,35 +7,33 @@ library
7 hs-source-dirs: src
7 hs-source-dirs: src
8 build-depends:
8 build-depends:
9 aeson
9 aeson
10− , ansi-terminal
11− , Cabal
12 , aeson-pretty
10 , aeson-pretty
11+ , ansi-terminal
13 , base
12 , base
14− , base16-bytestring
15− , binary
16 , bytestring
13 , bytestring
17− , obelisk-cliapp
14+ , Cabal
18 , containers
15 , containers
19 , data-default
16 , data-default
20 , directory
17 , directory
21 , either
18 , either
22 , errors
19 , errors
20+ , exceptions
23 , filepath
21 , filepath
22+ , git
24 , github
23 , github
25 , here
24 , here
26− , hit
27 , hnix
25 , hnix
28 , hpack
26 , hpack
29 , io-streams
27 , io-streams
30− , exceptions
31− , logging-effect
32 , lens
28 , lens
29+ , logging-effect
30+ , megaparsec
33 , modern-uri
31 , modern-uri
34 , monad-loops
32 , monad-loops
35 , mtl
33 , mtl
36− , megaparsec
37 , network
34 , network
38 , network-uri
35 , network-uri
36+ , obelisk-cliapp
39 , optparse-applicative
37 , optparse-applicative
40 , placeholders
38 , placeholders
41 , process
39 , process
@@ -46,6 +44,7 @@ library
46 , transformers
44 , transformers
47 , unix
45 , unix
48 , unordered-containers
46 , unordered-containers
47+ , which
49 , yaml
48 , yaml
50 exposed-modules:
49 exposed-modules:
51 Obelisk.App
50 Obelisk.App
@@ -57,8 +56,8 @@ library
57 Obelisk.Command.Thunk
56 Obelisk.Command.Thunk
58 Obelisk.Command.Utils
57 Obelisk.Command.Utils
59 Obelisk.Command.VmBuilder
58 Obelisk.Command.VmBuilder
60− -- -fobject-code is so that the StaticPointers extension can work in ghci
59+ Obelisk.Command.Preprocessor
61− ghc-options: -Wall -fobject-code
60+ ghc-options: -Wall
62
61
63executable ob
62executable ob
64 main-is: src-bin/ob.hs
63 main-is: src-bin/ob.hs
lib/command/src/Obelisk/App.hs+5−2
@@ -12,6 +12,7 @@ module Obelisk.App where
12
12
13import Control.Lens
13import Control.Lens
14import Control.Monad.Catch (MonadCatch, MonadMask, MonadThrow)
14import Control.Monad.Catch (MonadCatch, MonadMask, MonadThrow)
15+import Control.Monad.Fail (MonadFail)
15import Control.Monad.Reader (MonadIO, ReaderT (..), ask, runReaderT)
16import Control.Monad.Reader (MonadIO, ReaderT (..), ask, runReaderT)
16import Control.Monad.Writer (WriterT)
17import Control.Monad.Writer (WriterT)
17import Control.Monad.State (StateT)
18import Control.Monad.State (StateT)
@@ -61,10 +62,11 @@ newtype ObeliskT m a = ObeliskT
61 { unObeliskT :: ReaderT Obelisk (CliT ObeliskError m) a
62 { unObeliskT :: ReaderT Obelisk (CliT ObeliskError m) a
62 }
63 }
63 deriving
64 deriving
64− ( Functor, Applicative, Monad, MonadIO, MonadThrow, MonadCatch, MonadMask
65+ ( Functor, Applicative, Monad, MonadIO, MonadThrow, MonadCatch, MonadMask, MonadFail
65 , MonadLog Output -- CliLog
66 , MonadLog Output -- CliLog
66 , MonadError ObeliskError -- CliThrow ObeliskError
67 , MonadError ObeliskError -- CliThrow ObeliskError
67− , HasCliConfig ObeliskError)
68+ , HasCliConfig ObeliskError
69+ )
68
70
69instance MonadTrans ObeliskT where
71instance MonadTrans ObeliskT where
70 lift = ObeliskT . lift . lift
72 lift = ObeliskT . lift . lift
@@ -104,6 +106,7 @@ type MonadInfallibleObelisk m =
104type MonadObelisk m =
106type MonadObelisk m =
105 ( MonadInfallibleObelisk m
107 ( MonadInfallibleObelisk m
106 , CliThrow ObeliskError m
108 , CliThrow ObeliskError m
109+ , MonadFail m
107 )
110 )
108
111
109getObeliskUserStateDir :: IO FilePath
112getObeliskUserStateDir :: IO FilePath
lib/command/src/Obelisk/Command.hs+122−86
@@ -3,22 +3,16 @@
3{-# LANGUAGE MultiParamTypeClasses #-}
3{-# LANGUAGE MultiParamTypeClasses #-}
4{-# LANGUAGE OverloadedStrings #-}
4{-# LANGUAGE OverloadedStrings #-}
5{-# LANGUAGE ScopedTypeVariables #-}
5{-# LANGUAGE ScopedTypeVariables #-}
6−{-# LANGUAGE StaticPointers #-}
7−{-# LANGUAGE TypeApplications #-}
8module Obelisk.Command where
6module Obelisk.Command where
9
7
10import Control.Monad
8import Control.Monad
11import Control.Monad.IO.Class (liftIO)
9import Control.Monad.IO.Class (liftIO)
12−import qualified Data.Binary as Binary
13import Data.Bool (bool)
10import Data.Bool (bool)
14−import qualified Data.ByteString.Base16 as Base16
11+import Data.Foldable (for_)
15−import qualified Data.ByteString.Lazy as LBS
16import Data.List
12import Data.List
17−import Data.Maybe (catMaybes)
18import qualified Data.Text as T
13import qualified Data.Text as T
19−import Data.Text.Encoding
20−import GHC.StaticPtr
21import Options.Applicative
14import Options.Applicative
15+import Options.Applicative.Help.Pretty (text, (<$$>))
22import System.Directory
16import System.Directory
23import System.Environment
17import System.Environment
24import System.FilePath
18import System.FilePath
@@ -28,13 +22,12 @@ import System.Posix.Process (executeFile)
28
22
29import Obelisk.App
23import Obelisk.App
30import Obelisk.CliApp
24import Obelisk.CliApp
31−import Obelisk.CliApp.Demo (cliDemo)
32import Obelisk.Command.Deploy
25import Obelisk.Command.Deploy
33import Obelisk.Command.Project
26import Obelisk.Command.Project
34import Obelisk.Command.Run
27import Obelisk.Command.Run
35import Obelisk.Command.Thunk
28import Obelisk.Command.Thunk
36−import Obelisk.Command.Utils
37import qualified Obelisk.Command.VmBuilder as VmBuilder
29import qualified Obelisk.Command.VmBuilder as VmBuilder
30+import qualified Obelisk.Command.Preprocessor as Preprocessor
38
31
39
32
40data Args = Args
33data Args = Args
@@ -88,50 +81,43 @@ data ObCommand
88 = ObCommand_Init InitSource Bool
81 = ObCommand_Init InitSource Bool
89 | ObCommand_Deploy DeployCommand
82 | ObCommand_Deploy DeployCommand
90 | ObCommand_Run
83 | ObCommand_Run
84+ | ObCommand_Profile String [String]
91 | ObCommand_Thunk ThunkCommand
85 | ObCommand_Thunk ThunkCommand
92 | ObCommand_Repl
86 | ObCommand_Repl
93 | ObCommand_Watch
87 | ObCommand_Watch
88+ | ObCommand_Shell ShellOpts
89+ | ObCommand_Doc String [String] -- shell and list of packages
90+ | ObCommand_Hoogle String Int -- shell and port
94 | ObCommand_Internal ObInternal
91 | ObCommand_Internal ObInternal
95 deriving Show
92 deriving Show
96
93
97data ObInternal
94data ObInternal
98− = ObInternal_RunStaticIO StaticKey
95+ -- the preprocessor argument syntax is also handled outside
99− | ObInternal_CLIDemo
96+ -- optparse-applicative, but it shouldn't ever conflict with another syntax
97+ = ObInternal_ApplyPackages String String String [String]
100 deriving Show
98 deriving Show
101
99
102−inNixShell' :: MonadObelisk m => StaticPtr (ObeliskT IO ()) -> m ()
103−inNixShell' p = withProjectRoot "." $ \root -> do
104− cmd <- liftIO $ unwords <$> mkCmd -- TODO: shell escape instead of unwords
105− projectShell root False "ghc" cmd
106− where
107− mkCmd = do
108− argsCfg <- getArgsConfig
109− myArgs <- getArgs
110− obArgs <- parseCLIArgs argsCfg myArgs
111− progName <- getObeliskExe
112− return $ progName : catMaybes
113− [ Just "--no-handoff"
114− , bool Nothing (Just "--verbose") $ _args_verbose obArgs
115− , Just "internal"
116− , Just "run-static-io"
117− , Just $ encodeStaticKey $ staticKey p
118− ]
119−
120obCommand :: ArgsConfig -> Parser ObCommand
100obCommand :: ArgsConfig -> Parser ObCommand
121obCommand cfg = hsubparser
101obCommand cfg = hsubparser
122− (mconcat
102+ (mconcat
123− [ command "init" $ info (ObCommand_Init <$> initSource <*> initForce) $ progDesc "Initialize an Obelisk project"
103+ [ command "init" $ info (ObCommand_Init <$> initSource <*> initForce) $ progDesc "Initialize an Obelisk project"
124− , command "deploy" $ info (ObCommand_Deploy <$> deployCommand cfg) $ progDesc "Prepare a deployment for an Obelisk project"
104+ , command "deploy" $ info (ObCommand_Deploy <$> deployCommand cfg) $ progDesc "Prepare a deployment for an Obelisk project"
125− , command "run" $ info (pure ObCommand_Run) $ progDesc "Run current project in development mode"
105+ , command "run" $ info (pure ObCommand_Run) $ progDesc "Run current project in development mode"
106+ , command "profile" $ info (uncurry ObCommand_Profile <$> profileCommand) $ progDesc "Run current project with profiling enabled"
126− , command "thunk" $ info (ObCommand_Thunk <$> thunkCommand) $ progDesc "Manipulate thunk directories"
107+ , command "thunk" $ info (ObCommand_Thunk <$> thunkCommand) $ progDesc "Manipulate thunk directories"
127− , command "repl" $ info (pure ObCommand_Repl) $ progDesc "Open an interactive interpreter"
108+ , command "repl" $ info (pure ObCommand_Repl) $ progDesc "Open an interactive interpreter"
128− , command "watch" $ info (pure ObCommand_Watch) $ progDesc "Watch current project for errors and warnings"
109+ , command "watch" $ info (pure ObCommand_Watch) $ progDesc "Watch current project for errors and warnings"
129− ])
110+ , command "shell" $ info (ObCommand_Shell <$> shellOpts) $ progDesc "Enter a shell with project dependencies"
130− <|> subparser
111+ , command "doc" $ info (ObCommand_Doc <$> shellFlags <*> packageNames) $
131− (mconcat
112+ progDesc "List paths to haddock documentation for specified packages"
132− [ internal
113+ <> footerDoc (Just $
133− , command "internal" (info (ObCommand_Internal <$> internalCommand) mempty)
114+ text "Hint: To open the documentation you can pipe the output of this command like"
134− ])
115+ <$$> text "ob doc reflex reflex-dom-core | xargs -n1 xdg-open")
116+ , command "hoogle" $ info (ObCommand_Hoogle <$> shellFlags <*> portOpt 8080) $ progDesc "Run a hoogle server locally for your project's dependency tree"
117+ ])
118+
119+packageNames :: Parser [String]
120+packageNames = some (strArgument (metavar "PACKAGE-NAME..."))
135
121
136deployCommand :: ArgsConfig -> Parser DeployCommand
122deployCommand :: ArgsConfig -> Parser DeployCommand
137deployCommand cfg = hsubparser $ mconcat
123deployCommand cfg = hsubparser $ mconcat
@@ -143,7 +129,7 @@ deployCommand cfg = hsubparser $ mconcat
143 where
129 where
144 platformP = hsubparser $ mconcat
130 platformP = hsubparser $ mconcat
145 [ command "android" $ info (pure (Android, [])) mempty
131 [ command "android" $ info (pure (Android, [])) mempty
146− , command "ios" $ info ((,) <$> pure IOS <*> (fmap pure $ strArgument (metavar "TEAMID" <> help "Your Team ID - found in the Apple developer portal"))) mempty
132+ , command "ios" $ info ((,) <$> pure IOS <*> fmap pure (strArgument (metavar "TEAMID" <> help "Your Team ID - found in the Apple developer portal"))) mempty
147 ]
133 ]
148
134
149 remoteBuilderParser :: Parser (Maybe RemoteBuilder)
135 remoteBuilderParser :: Parser (Maybe RemoteBuilder)
@@ -165,8 +151,8 @@ deployCommand cfg = hsubparser $ mconcat
165
151
166deployInitOpts :: Parser DeployInitOpts
152deployInitOpts :: Parser DeployInitOpts
167deployInitOpts = DeployInitOpts
153deployInitOpts = DeployInitOpts
168− <$> strArgument (action "directory" <> metavar "DEPLOYDIR" <> help "Path to a directory that it will create")
154+ <$> strArgument (action "directory" <> metavar "DEPLOYDIR" <> help "Path to a directory where the deployment repository will be initialized")
169− <*> strOption (long "ssh-key" <> action "file" <> metavar "SSHKEY" <> help "Path to an ssh key that it will symlink to")
155+ <*> strOption (long "ssh-key" <> action "file" <> metavar "SSHKEY" <> help "Path to an SSH key that will be *copied* to the deployment repository")
170 <*> some (strOption (long "hostname" <> metavar "HOSTNAME" <> help "hostname of the deployment target"))
156 <*> some (strOption (long "hostname" <> metavar "HOSTNAME" <> help "hostname of the deployment target"))
171 <*> strOption (long "route" <> metavar "PUBLICROUTE" <> help "Publicly accessible URL of your app")
157 <*> strOption (long "route" <> metavar "PUBLICROUTE" <> help "Publicly accessible URL of your app")
172 <*> strOption (long "admin-email" <> metavar "ADMINEMAIL" <> help "Email address where administrative alerts will be sent")
158 <*> strOption (long "admin-email" <> metavar "ADMINEMAIL" <> help "Email address where administrative alerts will be sent")
@@ -193,11 +179,24 @@ data DeployInitOpts = DeployInitOpts
193 }
179 }
194 deriving Show
180 deriving Show
195
181
196−internalCommand :: Parser ObInternal
182+profileCommand :: Parser (String, [String])
197−internalCommand = subparser $ mconcat
183+profileCommand = (,)
198− [ command "run-static-io" $ info (ObInternal_RunStaticIO <$> argument (eitherReader decodeStaticKey) (action "static-key")) mempty
184+ <$> strOption
199− , command "clidemo" $ info (pure ObInternal_CLIDemo) mempty
185+ ( long "output"
200− ]
186+ <> short 'o'
187+ <> help "Base output to use for profiling output. Suffixes are added to this based on the profiling type. Defaults to a timestamped path in the profile/ directory in the project's root."
188+ <> metavar "PATH"
189+ <> value "profile/%Y-%m-%dT%H:%M:%S"
190+ <> showDefault
191+ )
192+ <*> (words <$> strOption
193+ ( long "rts-flags"
194+ <> help "RTS Flags to pass to the executable."
195+ <> value "-p -hc"
196+ <> metavar "FLAGS"
197+ <> showDefault
198+ ))
199+
201
200
202--TODO: Result should provide normalised path and also original user input for error reporting.
201--TODO: Result should provide normalised path and also original user input for error reporting.
203thunkDirectoryParser :: Parser FilePath
202thunkDirectoryParser :: Parser FilePath
@@ -207,33 +206,70 @@ thunkDirectoryParser = fmap (dropTrailingPathSeparator . normalise) . strArgumen
207 , help "Path to directory containing thunk data"
206 , help "Path to directory containing thunk data"
208 ]
207 ]
209
208
209+thunkConfig :: Parser ThunkConfig
210+thunkConfig = ThunkConfig
211+ <$>
212+ ( flag' (Just True) (long "private" <> help "Mark thunks as pointing to a private repository")
213+ <|> flag' (Just False) (long "public" <> help "Mark thunks as pointing to a public repository")
214+ <|> pure Nothing
215+ )
216+
217+thunkUpdateConfig :: Parser ThunkUpdateConfig
218+thunkUpdateConfig = ThunkUpdateConfig
219+ <$> optional (strOption (long "branch" <> metavar "BRANCH" <> help "Use the given branch when looking for the latest revision"))
220+ <*> thunkConfig
221+
222+thunkPackConfig :: Parser ThunkPackConfig
223+thunkPackConfig = ThunkPackConfig
224+ <$> 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.")
225+ <*> thunkConfig
226+
210data ThunkCommand
227data ThunkCommand
211− = ThunkCommand_Update [FilePath] (Maybe String)
228+ = ThunkCommand_Update [FilePath] ThunkUpdateConfig
212 | ThunkCommand_Unpack [FilePath]
229 | ThunkCommand_Unpack [FilePath]
213− | ThunkCommand_Pack [FilePath]
230+ | ThunkCommand_Pack [FilePath] ThunkPackConfig
214 deriving Show
231 deriving Show
215
232
216thunkCommand :: Parser ThunkCommand
233thunkCommand :: Parser ThunkCommand
217thunkCommand = hsubparser $ mconcat
234thunkCommand = hsubparser $ mconcat
218− [ command "update" $ info (ThunkCommand_Update <$> some thunkDirectoryParser <*> optional (strOption (long "branch" <> metavar "BRANCH"))) $ progDesc "Update thunk to latest revision available"
235+ [ command "update" $ info (ThunkCommand_Update <$> some thunkDirectoryParser <*> thunkUpdateConfig) $ progDesc "Update thunk to latest revision available"
219 , command "unpack" $ info (ThunkCommand_Unpack <$> some thunkDirectoryParser) $ progDesc "Unpack thunk into git checkout of revision it points to"
236 , command "unpack" $ info (ThunkCommand_Unpack <$> some thunkDirectoryParser) $ progDesc "Unpack thunk into git checkout of revision it points to"
220− , command "pack" $ info (ThunkCommand_Pack <$> some thunkDirectoryParser) $ progDesc "Pack git checkout into thunk that points at the current branch's upstream"
237+ , command "pack" $ info (ThunkCommand_Pack <$> some thunkDirectoryParser <*> thunkPackConfig) $ progDesc "Pack git checkout into thunk that points at the current branch's upstream"
221 ]
238 ]
222
239
240+data ShellOpts
241+ = ShellOpts
242+ { _shellOpts_shell :: String
243+ , _shellOpts_command :: Maybe String
244+ }
245+ deriving Show
246+
247+shellFlags :: Parser String
248+shellFlags =
249+ flag' "ghc" (long "ghc" <> help "Enter a shell environment having ghc (default)")
250+ <|> flag "ghc" "ghcjs" (long "ghcjs" <> help "Enter a shell having ghcjs rather than ghc")
251+ <|> strOption (short 'A' <> long "argument" <> metavar "NIXARG" <> help "Use the environment specified by the given nix argument of `shells'")
252+
253+shellOpts :: Parser ShellOpts
254+shellOpts = ShellOpts
255+ <$> shellFlags
256+ <*> optional (strArgument (metavar "COMMAND"))
257+
258+portOpt :: Int -> Parser Int
259+portOpt dfault = option auto (long "port" <> short 'p' <> help "Port number for server" <> showDefault <> value dfault <> metavar "INT")
260+
223parserPrefs :: ParserPrefs
261parserPrefs :: ParserPrefs
224parserPrefs = defaultPrefs
262parserPrefs = defaultPrefs
225 { prefShowHelpOnEmpty = True
263 { prefShowHelpOnEmpty = True
226 }
264 }
227
265
228−parseCLIArgs :: ArgsConfig -> [String] -> IO Args
229−parseCLIArgs cfg as = pure as >>= handleParseResult . execParserPure parserPrefs (argsInfo cfg)
230−
231-- | Create an Obelisk config for the current process.
266-- | Create an Obelisk config for the current process.
232mkObeliskConfig :: IO Obelisk
267mkObeliskConfig :: IO Obelisk
233mkObeliskConfig = do
268mkObeliskConfig = do
234 cliArgs <- getArgs
269 cliArgs <- getArgs
235 -- This function should not use argument parser (full argument parsing happens post handoff)
270 -- This function should not use argument parser (full argument parsing happens post handoff)
271+ -- TODO: See if we can use the argument parser with a subset of the parsers to get logging level out.
236− let logLevel = toLogLevel $ "-v" `elem` cliArgs
272+ let logLevel = toLogLevel $ any (`elem` ["-v", "--verbose"]) cliArgs
237 notInteractive <- not <$> isInteractiveTerm
273 notInteractive <- not <$> isInteractiveTerm
238 cliConf <- newCliConfig logLevel notInteractive notInteractive $ \case
274 cliConf <- newCliConfig logLevel notInteractive notInteractive $ \case
239 ObeliskError_ProcessError (ProcessFailure p code) ann ->
275 ObeliskError_ProcessError (ProcessFailure p code) ann ->
@@ -298,6 +334,9 @@ main' argsCfg = do
298 liftIO $ executeFile impl False ("--no-handoff" : myArgs) Nothing
334 liftIO $ executeFile impl False ("--no-handoff" : myArgs) Nothing
299 case myArgs of
335 case myArgs of
300 "--no-handoff" : as -> go as -- If we've been told not to hand off, don't hand off
336 "--no-handoff" : as -> go as -- If we've been told not to hand off, don't hand off
337+ origPath:inPath:outPath:preprocessorName:packagePaths
338+ | preprocessorName == preprocessorIdentifier && any (\c -> c == '.' || c == pathSeparator) origPath ->
339+ ob $ ObCommand_Internal $ ObInternal_ApplyPackages origPath inPath outPath packagePaths
301 a:as -- Otherwise bash completion would always hand-off even if the user isn't trying to
340 a:as -- Otherwise bash completion would always hand-off even if the user isn't trying to
302 | "--bash-completion" `isPrefixOf` a
341 | "--bash-completion" `isPrefixOf` a
303 && "--no-handoff" `elem` as -> go (a:as)
342 && "--no-handoff" `elem` as -> go (a:as)
@@ -324,14 +363,13 @@ ob = \case
324 Right (ThunkData_Packed ptr) -> return ptr
363 Right (ThunkData_Packed ptr) -> return ptr
325 Right (ThunkData_Checkout (Just ptr)) -> return ptr
364 Right (ThunkData_Checkout (Just ptr)) -> return ptr
326 Right (ThunkData_Checkout Nothing) ->
365 Right (ThunkData_Checkout Nothing) ->
327− getThunkPtr' False root
366+ getThunkPtr False root Nothing
328 let sshKeyPath = _deployInitOpts_sshKey deployOpts
367 let sshKeyPath = _deployInitOpts_sshKey deployOpts
329 hostname = _deployInitOpts_hostname deployOpts
368 hostname = _deployInitOpts_hostname deployOpts
330 route = _deployInitOpts_route deployOpts
369 route = _deployInitOpts_route deployOpts
331 adminEmail = _deployInitOpts_adminEmail deployOpts
370 adminEmail = _deployInitOpts_adminEmail deployOpts
332 enableHttps = _deployInitOpts_enableHttps deployOpts
371 enableHttps = _deployInitOpts_enableHttps deployOpts
333− deployInit thunkPtr (root </> "config") deployDir
334− sshKeyPath hostname route adminEmail enableHttps
372+ deployInit thunkPtr deployDir sshKeyPath hostname route adminEmail enableHttps
335 DeployCommand_Push remoteBuilder -> do
373 DeployCommand_Push remoteBuilder -> do
336 deployPath <- liftIO $ canonicalizePath "."
374 deployPath <- liftIO $ canonicalizePath "."
337 deployPush deployPath $ case remoteBuilder of
375 deployPush deployPath $ case remoteBuilder of
@@ -339,35 +377,33 @@ ob = \case
339 Just RemoteBuilder_ObeliskVM -> (:[]) <$> VmBuilder.getNixBuildersArg
377 Just RemoteBuilder_ObeliskVM -> (:[]) <$> VmBuilder.getNixBuildersArg
340 DeployCommand_Update -> deployUpdate "."
378 DeployCommand_Update -> deployUpdate "."
341 DeployCommand_Test (platform, extraArgs) -> deployMobile platform extraArgs
379 DeployCommand_Test (platform, extraArgs) -> deployMobile platform extraArgs
342− ObCommand_Run -> inNixShell' $ static run
380+ ObCommand_Run -> run
343− -- inNixShell ($(mkClosure 'ghcidAction) ())
381+ ObCommand_Profile basePath rtsFlags -> profile basePath rtsFlags
344 ObCommand_Thunk tc -> case tc of
382 ObCommand_Thunk tc -> case tc of
345− ThunkCommand_Update thunks mBranch -> mapM_ ((flip updateThunkToLatest) mBranch) thunks
383+ ThunkCommand_Update thunks config -> for_ thunks (updateThunkToLatest config)
346− ThunkCommand_Unpack thunks -> mapM_ unpackThunk thunks
384+ ThunkCommand_Unpack thunks -> for_ thunks unpackThunk
347− ThunkCommand_Pack thunks -> forM_ thunks packThunk
385+ ThunkCommand_Pack thunks config -> for_ thunks (packThunk config)
348 ObCommand_Repl -> runRepl
386 ObCommand_Repl -> runRepl
349− ObCommand_Watch -> inNixShell' $ static runWatch
387+ ObCommand_Watch -> runWatch
388+ ObCommand_Shell so -> withProjectRoot "." $ \root ->
389+ projectShell root False (_shellOpts_shell so) (_shellOpts_command so)
390+ ObCommand_Doc shell' pkgs -> withProjectRoot "." $ \root ->
391+ projectShell root False shell' (Just $ haddockCommand pkgs)
392+ ObCommand_Hoogle shell' port -> withProjectRoot "." $ \root -> do
393+ nixShellWithHoogle root True shell' $ Just $ "hoogle server -p " <> show port <> " --local"
350 ObCommand_Internal icmd -> case icmd of
394 ObCommand_Internal icmd -> case icmd of
351− ObInternal_RunStaticIO k -> liftIO (unsafeLookupStaticPtr @(ObeliskT IO ()) k) >>= \case
395+ ObInternal_ApplyPackages origPath inPath outPath packagePaths -> do
352− Nothing -> failWith $ "ObInternal_RunStaticIO: no such StaticKey: " <> T.pack (show k)
396+ liftIO $ Preprocessor.applyPackages origPath inPath outPath packagePaths
353− Just p -> do
397+
354− c <- getObelisk
398+haddockCommand :: [String] -> String
355− liftIO $ runObelisk c $ deRefStaticPtr p
399+haddockCommand pkgs = unwords
356− ObInternal_CLIDemo -> cliDemo
400+ [ "for p in"
401+ , unwords [getHaddockPath p ++ "/index.html" | p <- pkgs]
402+ , "; do echo $p; done"
403+ ]
404+ where getHaddockPath p = "$(ghc-pkg field " ++ p ++ " haddock-html --simple-output)"
357
405
358--TODO: Clean up all the magic strings throughout this codebase
406--TODO: Clean up all the magic strings throughout this codebase
359
407
360getArgsConfig :: IO ArgsConfig
408getArgsConfig :: IO ArgsConfig
361getArgsConfig = pure $ ArgsConfig { _argsConfig_enableVmBuilderByDefault = System.Info.os == "darwin" }
409getArgsConfig = pure $ ArgsConfig { _argsConfig_enableVmBuilderByDefault = System.Info.os == "darwin" }
362−
363−encodeStaticKey :: StaticKey -> String
364−encodeStaticKey = T.unpack . decodeUtf8 . Base16.encode . LBS.toStrict . Binary.encode
365−
366−-- TODO: Use failWith in place of fail to be consistent.
367−decodeStaticKey :: String -> Either String StaticKey
368−decodeStaticKey s = case Base16.decode $ encodeUtf8 $ T.pack s of
369− (b, "") -> case Binary.decodeOrFail $ LBS.fromStrict b of
370− Right ("", _, a) -> pure a
371− Right _ -> fail "decodeStaticKey: Binary.decodeOrFail didn't consume all input"
372− Left (_, _, e) -> fail $ "decodeStaticKey: Binary.decodeOrFail failed: " <> show e
373− _ -> fail $ "decodeStaticKey: could not decode hex string: " <> show s
lib/command/src/Obelisk/Command/Deploy.hs+89−83
@@ -4,92 +4,101 @@
4{-# LANGUAGE ScopedTypeVariables #-}
4{-# LANGUAGE ScopedTypeVariables #-}
5{-# LANGUAGE ViewPatterns #-}
5{-# LANGUAGE ViewPatterns #-}
6{-# LANGUAGE DeriveGeneric #-}
6{-# LANGUAGE DeriveGeneric #-}
7−{-# LANGUAGE TypeApplications #-}
8module Obelisk.Command.Deploy where
7module Obelisk.Command.Deploy where
9
8
10import Control.Lens
9import Control.Lens
11import Control.Monad
10import Control.Monad
12−import Control.Monad.Catch (Exception (displayException), MonadThrow, throwM, try, bracket_)
11+import Control.Monad.Catch (Exception (displayException), MonadThrow, bracket, throwM, try)
13import Control.Monad.IO.Class (liftIO)
12import Control.Monad.IO.Class (liftIO)
14import Data.Aeson (FromJSON, ToJSON, encode, eitherDecode)
13import Data.Aeson (FromJSON, ToJSON, encode, eitherDecode)
15import Data.Bits
14import Data.Bits
16import qualified Data.ByteString.Lazy as BSL
15import qualified Data.ByteString.Lazy as BSL
17import Data.Default
16import Data.Default
18−import qualified Data.HashMap.Strict as HM
19import qualified Data.Map as Map
17import qualified Data.Map as Map
20−import Data.Text (Text)
21import qualified Data.Set as Set
18import qualified Data.Set as Set
22import qualified Data.Text as T
19import qualified Data.Text as T
23import qualified Data.Text.IO as T
20import qualified Data.Text.IO as T
24import GHC.Generics
21import GHC.Generics
25−import Nix.Convert
26−import Nix.Pretty
27−import Nix.Value
28import System.Directory
22import System.Directory
29−import System.Environment (getEnvironment)
30import System.FilePath
23import System.FilePath
31import System.IO
24import System.IO
32import System.Posix.Files
25import System.Posix.Files
33−import System.Process (delegate_ctlc, env, proc, cwd)
34import Text.URI (URI)
26import Text.URI (URI)
35import qualified Text.URI as URI
27import qualified Text.URI as URI
36import Text.URI.Lens
28import Text.URI.Lens
37
29
38import Obelisk.App (MonadObelisk)
30import Obelisk.App (MonadObelisk)
31+import Obelisk.CliApp (
39−import Obelisk.CliApp (Severity (..), callProcessAndLogOutput, failWith, putLog, withSpinner)
32+ Severity (..), callProcessAndLogOutput, failWith, proc, putLog,
33+ setCwd, setDelegateCtlc, setEnvOverride, withSpinner)
40import Obelisk.Command.Nix
34import Obelisk.Command.Nix
41import Obelisk.Command.Project
35import Obelisk.Command.Project
42import Obelisk.Command.Thunk
36import Obelisk.Command.Thunk
43import Obelisk.Command.Utils
37import Obelisk.Command.Utils
44
38
39+
45deployInit
40deployInit
46 :: MonadObelisk m
41 :: MonadObelisk m
47 => ThunkPtr
42 => ThunkPtr
48 -> FilePath
43 -> FilePath
49 -> FilePath
44 -> FilePath
50− -> FilePath
51 -> [String] -- ^ hostnames
45 -> [String] -- ^ hostnames
52 -> String -- ^ route
46 -> String -- ^ route
53 -> String -- ^ admin email
47 -> String -- ^ admin email
54 -> Bool -- ^ enable https
48 -> Bool -- ^ enable https
55 -> m ()
49 -> m ()
56−deployInit thunkPtr configDir deployDir sshKeyPath hostnames route adminEmail enableHttps = do
50+deployInit thunkPtr deployDir sshKeyPath hostnames route adminEmail enableHttps = do
51+ liftIO $ createDirectoryIfMissing True deployDir
57− (hasConfigDir, localKey) <- withSpinner ("Preparing " <> T.pack deployDir) $ do
52+ localKey <- withSpinner ("Preparing " <> T.pack deployDir) $ do
58− hasConfigDir <- liftIO $ do
59− createDirectoryIfMissing True deployDir
60− doesDirectoryExist configDir
61 localKey <- liftIO (doesFileExist sshKeyPath) >>= \case
53 localKey <- liftIO (doesFileExist sshKeyPath) >>= \case
62 False -> failWith $ T.pack $ "ob deploy init: file does not exist: " <> sshKeyPath
54 False -> failWith $ T.pack $ "ob deploy init: file does not exist: " <> sshKeyPath
63 True -> pure $ deployDir </> "ssh_key"
55 True -> pure $ deployDir </> "ssh_key"
64 callProcessAndLogOutput (Notice, Error) $
56 callProcessAndLogOutput (Notice, Error) $
65− proc "cp" [sshKeyPath, localKey]
57+ proc cp [sshKeyPath, localKey]
66 liftIO $ setFileMode localKey $ ownerReadMode .|. ownerWriteMode
58 liftIO $ setFileMode localKey $ ownerReadMode .|. ownerWriteMode
67− return $ (hasConfigDir, localKey)
59+ return localKey
68 withSpinner "Validating configuration" $ do
60 withSpinner "Validating configuration" $ do
69 void $ getHostFromRoute enableHttps route -- make sure that hostname is present
61 void $ getHostFromRoute enableHttps route -- make sure that hostname is present
70 forM_ hostnames $ \hostname -> do
62 forM_ hostnames $ \hostname -> do
71 putLog Notice $ "Verifying host keys (" <> T.pack hostname <> ")"
63 putLog Notice $ "Verifying host keys (" <> T.pack hostname <> ")"
72 -- Note: we can't use a spinner here as this function will prompt the user.
64 -- Note: we can't use a spinner here as this function will prompt the user.
73 verifyHostKey (deployDir </> "backend_known_hosts") localKey hostname
65 verifyHostKey (deployDir </> "backend_known_hosts") localKey hostname
66+ --IMPORTANT: We cannot copy config directory from the development project to
67+ --the deployment directory. If we do, it's very likely someone will
68+ --accidentally create a production deployment that uses development
69+ --credentials to connect to some resources. This could result in, e.g.,
70+ --production data backed up to a dev environment.
74− when hasConfigDir $ withSpinner "Importing project configuration" $ do
71+ withSpinner "Creating project configuration directories" $ do
75 callProcessAndLogOutput (Notice, Error) $
72 callProcessAndLogOutput (Notice, Error) $
73+ proc "mkdir" [ "-p"
74+ , deployDir </> "config" </> "backend"
75+ , deployDir </> "config" </> "common"
76− proc "cp" [ "-r" , "-T" , configDir , deployDir </> "config"]
76+ , deployDir </> "config" </> "frontend"
77+ ]
78+
79+ let srcDir = deployDir </> "src"
80+ withSpinner ("Creating source thunk (" <> T.pack (makeRelative deployDir srcDir) <> ")") $ liftIO $ do
81+ createThunk srcDir thunkPtr
82+ setupObeliskImpl deployDir
83+
77 withSpinner "Writing deployment configuration" $ do
84 withSpinner "Writing deployment configuration" $ do
78 writeDeployConfig deployDir "backend_hosts" $ unlines hostnames
85 writeDeployConfig deployDir "backend_hosts" $ unlines hostnames
79 writeDeployConfig deployDir "enable_https" $ show enableHttps
86 writeDeployConfig deployDir "enable_https" $ show enableHttps
80 writeDeployConfig deployDir "admin_email" adminEmail
87 writeDeployConfig deployDir "admin_email" adminEmail
81− writeDeployConfig deployDir ("config" </> "common" </> "route") $ route
88+ writeDeployConfig deployDir ("config" </> "common" </> "route") route
82− withSpinner "Creating source thunk (./src)" $ liftIO $ do
89+ writeDeployConfig deployDir "module.nix" $
83− createThunk (deployDir </> "src") thunkPtr
90+ "(import " <> toNixPath (makeRelative deployDir srcDir) <> " {}).obelisk.serverModules.mkBaseEc2"
84− setupObeliskImpl deployDir
91+
85 withSpinner ("Initializing git repository (" <> T.pack deployDir <> ")") $
92 withSpinner ("Initializing git repository (" <> T.pack deployDir <> ")") $
86 initGit deployDir
93 initGit deployDir
87
94
88setupObeliskImpl :: FilePath -> IO ()
95setupObeliskImpl :: FilePath -> IO ()
89setupObeliskImpl deployDir = do
96setupObeliskImpl deployDir = do
97+ let
90− let implDir = toImplDir deployDir
98+ implDir = toImplDir deployDir
99+ goBackUp = foldr (</>) "" $ (".." <$) $ splitPath $ makeRelative deployDir implDir
91 createDirectoryIfMissing True implDir
100 createDirectoryIfMissing True implDir
92− writeFile (implDir </> "default.nix") "(import ../../src {}).obelisk"
101+ writeFile (implDir </> "default.nix") $ "(import " <> toNixPath (goBackUp </> "src") <> " {}).obelisk"
93
102
94deployPush :: MonadObelisk m => FilePath -> m [String] -> m ()
103deployPush :: MonadObelisk m => FilePath -> m [String] -> m ()
95deployPush deployPath getNixBuilders = do
104deployPush deployPath getNixBuilders = do
@@ -103,11 +112,13 @@ deployPush deployPath getNixBuilders = do
103 Right (ThunkData_Packed ptr) -> return ptr
112 Right (ThunkData_Packed ptr) -> return ptr
104 Right (ThunkData_Checkout _) -> do
113 Right (ThunkData_Checkout _) -> do
105 checkGitCleanStatus srcPath True >>= \case
114 checkGitCleanStatus srcPath True >>= \case
106− True -> packThunk srcPath
115+ True -> packThunk (ThunkPackConfig False (ThunkConfig Nothing)) srcPath
107 False -> failWith $ T.pack $ "ob deploy push: ensure " <> srcPath <> " has no pending changes and latest is pushed upstream."
116 False -> failWith $ T.pack $ "ob deploy push: ensure " <> srcPath <> " has no pending changes and latest is pushed upstream."
108 Left err -> failWith $ "ob deploy push: couldn't read src thunk: " <> T.pack (show err)
117 Left err -> failWith $ "ob deploy push: couldn't read src thunk: " <> T.pack (show err)
109 let version = show . _thunkRev_commit $ _thunkPtr_rev thunkPtr
118 let version = show . _thunkRev_commit $ _thunkPtr_rev thunkPtr
110 builders <- getNixBuilders
119 builders <- getNixBuilders
120+ let moduleFile = deployPath </> "module.nix"
121+ moduleFileExists <- liftIO $ doesFileExist moduleFile
111 buildOutputByHost <- ifor (Map.fromSet (const ()) hosts) $ \host () -> do
122 buildOutputByHost <- ifor (Map.fromSet (const ()) hosts) $ \host () -> do
112 --TODO: What does it mean if this returns more or less than 1 line of output?
123 --TODO: What does it mean if this returns more or less than 1 line of output?
113 [result] <- fmap lines $ nixCmd $ NixCmd_Build $ def
124 [result] <- fmap lines $ nixCmd $ NixCmd_Build $ def
@@ -117,13 +128,13 @@ deployPush deployPath getNixBuilders = do
117 , _target_expr = Nothing
128 , _target_expr = Nothing
118 }
129 }
119 & nixBuildConfig_outLink .~ OutLink_None
130 & nixBuildConfig_outLink .~ OutLink_None
120− & nixCmdConfig_args .~
131+ & nixCmdConfig_args .~ (
121 [ strArg "hostName" host
132 [ strArg "hostName" host
122 , strArg "adminEmail" adminEmail
133 , strArg "adminEmail" adminEmail
123 , strArg "routeHost" routeHost
134 , strArg "routeHost" routeHost
124 , strArg "version" version
135 , strArg "version" version
125 , boolArg "enableHttps" enableHttps
136 , boolArg "enableHttps" enableHttps
126− ]
137+ ] <> [rawArg "module" ("import " <> toNixPath moduleFile) | moduleFileExists ])
127 & nixCmdConfig_builders .~ builders
138 & nixCmdConfig_builders .~ builders
128 pure result
139 pure result
129 let knownHostsPath = deployPath </> "backend_known_hosts"
140 let knownHostsPath = deployPath </> "backend_known_hosts"
@@ -131,7 +142,7 @@ deployPush deployPath getNixBuilders = do
131 withSpinner "Uploading closures" $ ifor_ buildOutputByHost $ \host outputPath -> do
142 withSpinner "Uploading closures" $ ifor_ buildOutputByHost $ \host outputPath -> do
132 callProcess'
143 callProcess'
133 (Map.fromList [("NIX_SSHOPTS", unwords sshOpts)])
144 (Map.fromList [("NIX_SSHOPTS", unwords sshOpts)])
134− "nix-copy-closure" ["-v", "--to", "root@" <> host, "--gzip", outputPath]
145+ "nix-copy-closure" ["-v", "--to", "--use-substitutes", "root@" <> host, "--gzip", outputPath]
135 withSpinner "Uploading config" $ ifor_ buildOutputByHost $ \host _ -> do
146 withSpinner "Uploading config" $ ifor_ buildOutputByHost $ \host _ -> do
136 callProcessAndLogOutput (Notice, Warning) $
147 callProcessAndLogOutput (Notice, Warning) $
137 proc "rsync"
148 proc "rsync"
@@ -153,7 +164,7 @@ deployPush deployPath getNixBuilders = do
153 ]
164 ]
154 isClean <- checkGitCleanStatus deployPath True
165 isClean <- checkGitCleanStatus deployPath True
155 when (not isClean) $ do
166 when (not isClean) $ do
156− withSpinner "Commiting changes to Git" $ do
167+ withSpinner "Committing changes to Git" $ do
157 callProcessAndLogOutput (Debug, Error) $ proc "git"
168 callProcessAndLogOutput (Debug, Error) $ proc "git"
158 ["-C", deployPath, "add", "."]
169 ["-C", deployPath, "add", "."]
159 callProcessAndLogOutput (Debug, Error) $ proc "git"
170 callProcessAndLogOutput (Debug, Error) $ proc "git"
@@ -161,25 +172,11 @@ deployPush deployPath getNixBuilders = do
161 putLog Notice $ "Deployed => " <> T.pack route
172 putLog Notice $ "Deployed => " <> T.pack route
162 where
173 where
163 callProcess' envMap cmd args = do
174 callProcess' envMap cmd args = do
164− processEnv <- Map.toList . (envMap <>) . Map.fromList <$> liftIO getEnvironment
175+ let p = setEnvOverride (envMap <>) $ setDelegateCtlc True $ proc cmd args
165− let p = (proc cmd args) { delegate_ctlc = True, env = Just processEnv }
166 callProcessAndLogOutput (Notice, Notice) p
176 callProcessAndLogOutput (Notice, Notice) p
167
177
168deployUpdate :: MonadObelisk m => FilePath -> m ()
178deployUpdate :: MonadObelisk m => FilePath -> m ()
169−deployUpdate deployPath = updateThunkToLatest (deployPath </> "src") Nothing
179+deployUpdate deployPath = updateThunkToLatest (ThunkUpdateConfig Nothing (ThunkConfig Nothing)) (deployPath </> "src")
170−
171−keytoolToAndroidConfig :: KeytoolConfig -> HM.HashMap Text (NValueNF Identity)
172−keytoolToAndroidConfig conf = runIdentity $ do
173− path <- toValue $ Path $ _keytoolConfig_keystore conf
174− storepass <- toValue $ T.pack $ _keytoolConfig_storepass conf
175− alias <- toValue $ T.pack $ _keytoolConfig_alias conf
176− keypass <- toValue $ T.pack $ _keytoolConfig_keypass conf
177− return $ HM.fromList
178− [ ("storeFile", path)
179− , ("storePassword", storepass)
180− , ("keyAlias", alias)
181− , ("keyPassword", keypass)
182− ]
183
180
184data PlatformDeployment = Android | IOS
181data PlatformDeployment = Android | IOS
185 deriving (Show, Eq)
182 deriving (Show, Eq)
@@ -189,13 +186,13 @@ renderPlatformDeployment = \case
189 Android -> "android"
186 Android -> "android"
190 IOS -> "ios"
187 IOS -> "ios"
191
188
192−deployMobile :: MonadObelisk m => PlatformDeployment -> [String] -> m ()
189+deployMobile :: forall m. MonadObelisk m => PlatformDeployment -> [String] -> m ()
193deployMobile platform mobileArgs = withProjectRoot "." $ \root -> do
190deployMobile platform mobileArgs = withProjectRoot "." $ \root -> do
194 let srcDir = root </> "src"
191 let srcDir = root </> "src"
195 configDir = root </> "config"
192 configDir = root </> "config"
196 exists <- liftIO $ doesDirectoryExist srcDir
193 exists <- liftIO $ doesDirectoryExist srcDir
197 unless exists $ failWith "ob test should be run inside of a deploy directory"
194 unless exists $ failWith "ob test should be run inside of a deploy directory"
198− nixBuildTarget <- case platform of
195+ (nixBuildTarget, extraArgs) <- case platform of
199 Android -> do
196 Android -> do
200 let keystorePath = root </> "android_keystore.jks"
197 let keystorePath = root </> "android_keystore.jks"
201 keytoolConfPath = root </> "android_keytool_config.json"
198 keytoolConfPath = root </> "android_keytool_config.json"
@@ -214,48 +211,60 @@ deployMobile platform mobileArgs = withProjectRoot "." $ \root -> do
214 , _keytoolConfig_storepass = keyStorePassword
211 , _keytoolConfig_storepass = keyStorePassword
215 , _keytoolConfig_keypass = keyStorePassword
212 , _keytoolConfig_keypass = keyStorePassword
216 }
213 }
217− createKeystore root $ keyToolConf
214+ createKeystore root keyToolConf
218 liftIO $ BSL.writeFile keytoolConfPath $ encode keyToolConf
215 liftIO $ BSL.writeFile keytoolConfPath $ encode keyToolConf
219 checkKeytoolConfExist <- liftIO $ doesFileExist keytoolConfPath
216 checkKeytoolConfExist <- liftIO $ doesFileExist keytoolConfPath
220 unless checkKeytoolConfExist $ failWith "Missing android KeytoolConfig"
217 unless checkKeytoolConfExist $ failWith "Missing android KeytoolConfig"
221 keytoolConfContents <- liftIO $ BSL.readFile keytoolConfPath
218 keytoolConfContents <- liftIO $ BSL.readFile keytoolConfPath
222− liftIO $ putStrLn $ show keytoolConfContents
223− releaseKey <- case eitherDecode keytoolConfContents of
219+ keyArgs <- case eitherDecode keytoolConfContents :: Either String KeytoolConfig of
224 Left err -> failWith $ T.pack err
220 Left err -> failWith $ T.pack err
225− Right conf -> do
221+ Right conf -> pure
226− let nvset = toValue @(HM.HashMap Text (NValueNF Identity)) @Identity @(NValueNF Identity) $ keytoolToAndroidConfig conf
222+ [ "--sign"
227− return $ printNix $ runIdentity nvset
223+ , "--store-file", _keytoolConfig_keystore conf
224+ , "--store-password", _keytoolConfig_storepass conf
225+ , "--key-alias", _keytoolConfig_alias conf
226+ , "--key-password", _keytoolConfig_keypass conf
227+ ]
228 let expr = mconcat
228 let expr = mconcat
229− [ "with (import ", srcDir, " {});"
229+ [ "with (import ", toNixPath srcDir, " {});"
230− , "android.frontend.override (drv: { "
230+ , "android.frontend.override (drv: {"
231− , "releaseKey = (if builtins.isNull drv.releaseKey then {} else drv.releaseKey) // " <> releaseKey <> "; "
231+ , "isRelease = true;"
232+ , "staticSrc = (passthru.__androidWithConfig ", configDir, ").frontend.staticSrc;"
232− , "staticSrc = (passthru.__android ", configDir, ").frontend.staticSrc;"
233+ , "assets = (passthru.__androidWithConfig ", configDir, ").frontend.assets;"
233 , "})"
234 , "})"
234 ]
235 ]
235− return $ Target
236+ return (Target
236 { _target_path = Nothing
237 { _target_path = Nothing
237 , _target_attr = Nothing
238 , _target_attr = Nothing
238 , _target_expr = Just expr
239 , _target_expr = Just expr
239− }
240+ }, keyArgs)
240 IOS -> do
241 IOS -> do
241 let expr = mconcat
242 let expr = mconcat
242− [ "with (import ", srcDir, " {});"
243+ [ "with (import ", toNixPath srcDir, " {});"
243− , "ios.frontend.override (_: { staticSrc = (passthru.__ios ", configDir, ").frontend.staticSrc; })"
244+ , "ios.frontend.override (_: { staticSrc = (passthru.__iosWithConfig ", toNixPath configDir, ").frontend.staticSrc; })"
244 ]
245 ]
245− return $ Target
246+ return (Target
246 { _target_path = Nothing
247 { _target_path = Nothing
247 , _target_attr = Nothing
248 , _target_attr = Nothing
248 , _target_expr = Just expr
249 , _target_expr = Just expr
249− }
250+ }, [])
250 result <- nixCmd $ NixCmd_Build $ def
251 result <- nixCmd $ NixCmd_Build $ def
251 & nixBuildConfig_outLink .~ OutLink_None
252 & nixBuildConfig_outLink .~ OutLink_None
252 & nixCmdConfig_target .~ nixBuildTarget
253 & nixCmdConfig_target .~ nixBuildTarget
254+ let mobileArtifact = case platform of
255+ IOS -> "iOS App"
256+ Android -> "Android APK"
253− putLog Notice $ T.pack $ "Your recently built android apk can be found at the following path: " <> (show result)
257+ putLog Notice $ T.pack $ unwords ["Your recently built", mobileArtifact, "can be found at the following path:", show result]
254− callProcessAndLogOutput (Notice, Error) $ proc (result </> "bin" </> "deploy") mobileArgs
258+ callProcessAndLogOutput (Notice, Error) $ proc (result </> "bin" </> "deploy") (mobileArgs ++ extraArgs)
255 where
259 where
256− withEcho showEcho f = do
260+ withEcho showEcho f = bracket
261+ (do
257− prevEcho <- hGetEcho stdin
262+ prevEcho <- hGetEcho stdin
258− bracket_ (hSetEcho stdin showEcho) (hSetEcho stdin prevEcho) f
263+ hSetEcho stdin showEcho
264+ pure prevEcho
265+ )
266+ (hSetEcho stdin)
267+ (const f)
259
268
260data KeytoolConfig = KeytoolConfig
269data KeytoolConfig = KeytoolConfig
261 { _keytoolConfig_keystore :: FilePath
270 { _keytoolConfig_keystore :: FilePath
@@ -268,19 +277,16 @@ instance FromJSON KeytoolConfig
268instance ToJSON KeytoolConfig
277instance ToJSON KeytoolConfig
269
278
270createKeystore :: MonadObelisk m => FilePath -> KeytoolConfig -> m ()
279createKeystore :: MonadObelisk m => FilePath -> KeytoolConfig -> m ()
271−createKeystore root config = do
280+createKeystore root config =
272− let expr = "with (import " <> toImplDir root <> ").reflex-platform.nixpkgs; pkgs.mkShell { buildInputs = [ pkgs.jdk ]; }"
273− callProcessAndLogOutput (Notice,Notice) $ (proc "nix-shell" ["-E" , expr, "--run" , keytoolCmd]) { cwd = Just root }
281+ callProcessAndLogOutput (Notice, Notice) $ setCwd (Just root) $ proc jreKeyToolPath
274− where
275− keytoolCmd = processToShellString "keytool"
276− [ "-genkeypair", "-noprompt"
282+ [ "-genkeypair", "-noprompt"
277− , "-keystore", _keytoolConfig_keystore config
283+ , "-keystore", _keytoolConfig_keystore config
278− , "-keyalg", "RSA", "-keysize", "2048"
284+ , "-keyalg", "RSA", "-keysize", "2048"
279− , "-validity", "1000000000"
285+ , "-validity", "1000000"
280− , "-storepass", _keytoolConfig_storepass config
286+ , "-storepass", _keytoolConfig_storepass config
281− , "-alias", _keytoolConfig_alias config
287+ , "-alias", _keytoolConfig_alias config
282− , "-keypass", _keytoolConfig_keypass config
288+ , "-keypass", _keytoolConfig_keypass config
283− ]
289+ ]
284
290
285-- | Simplified deployment configuration mechanism. At one point we may revisit this.
291-- | Simplified deployment configuration mechanism. At one point we may revisit this.
286writeDeployConfig :: MonadObelisk m => FilePath -> FilePath -> String -> m ()
292writeDeployConfig :: MonadObelisk m => FilePath -> FilePath -> String -> m ()
lib/command/src/Obelisk/Command/Nix.hs+65−25
@@ -9,22 +9,33 @@
9{-# LANGUAGE TemplateHaskell #-}
9{-# LANGUAGE TemplateHaskell #-}
10{-# LANGUAGE TypeApplications #-}
10{-# LANGUAGE TypeApplications #-}
11module Obelisk.Command.Nix
11module Obelisk.Command.Nix
12− ( nixCmd
12+ ( Arg (..)
13− , NixCmd (..)
14− , nixCmdConfig_target
15− , nixCmdConfig_args
16− , nixCmdConfig_builders
17 , NixBuildConfig (..)
13 , NixBuildConfig (..)
18 , nixBuildConfig_common
14 , nixBuildConfig_common
19 , nixBuildConfig_outLink
15 , nixBuildConfig_outLink
16+ , NixCmd (..)
17+ , nixCmdConfig_args
18+ , nixCmdConfig_builders
19+ , nixCmdConfig_target
20+ , NixCommonConfig (..)
20 , NixInstantiateConfig (..)
21 , NixInstantiateConfig (..)
21 , nixInstantiateConfig_eval
22 , nixInstantiateConfig_eval
22− , NixCommonConfig (..)
23+ , NixShellConfig (..)
23− , Target (..)
24+ , nixShellConfig_common
25+ , nixShellConfig_pure
26+ , nixShellConfig_run
24 , OutLink (..)
27 , OutLink (..)
25− , Arg (..)
28+ , Target (..)
29+ , target_attr
30+ , target_expr
31+ , target_path
32+
26 , boolArg
33 , boolArg
34+ , nixCmd
35+ , nixCmdProc
36+ , nixCmdProc'
27 , rawArg
37 , rawArg
38+ , runNixShellConfig
28 , strArg
39 , strArg
29 ) where
40 ) where
30
41
@@ -37,7 +48,6 @@ import Data.List (intercalate)
37import Data.Maybe
48import Data.Maybe
38import Data.Monoid ((<>))
49import Data.Monoid ((<>))
39import qualified Data.Text as T
50import qualified Data.Text as T
40−import System.Process (proc)
41
51
42import Obelisk.App (MonadObelisk)
52import Obelisk.App (MonadObelisk)
43import Obelisk.CliApp
53import Obelisk.CliApp
@@ -63,10 +73,10 @@ data Arg
63 deriving (Eq, Show)
73 deriving (Eq, Show)
64
74
65strArg :: String -> String -> Arg
75strArg :: String -> String -> Arg
66−strArg k = Arg_Str k
76+strArg = Arg_Str
67
77
68rawArg :: String -> String -> Arg
78rawArg :: String -> String -> Arg
69−rawArg k = Arg_Expr k
79+rawArg = Arg_Expr
70
80
71boolArg :: String -> Bool -> Arg
81boolArg :: String -> Bool -> Arg
72boolArg k = Arg_Expr k . bool "false" "true"
82boolArg k = Arg_Expr k . bool "false" "true"
@@ -86,7 +96,7 @@ makeClassy ''NixCommonConfig
86instance Default NixCommonConfig where
96instance Default NixCommonConfig where
87 def = NixCommonConfig def mempty mempty
97 def = NixCommonConfig def mempty mempty
88
98
89−runNixCommonConfig :: NixCommonConfig -> [FilePath]
99+runNixCommonConfig :: NixCommonConfig -> [String]
90runNixCommonConfig cfg = mconcat [maybeToList path, attrArg, exprArg, args, buildersArg]
100runNixCommonConfig cfg = mconcat [maybeToList path, attrArg, exprArg, args, buildersArg]
91 where
101 where
92 path = _target_path $ _nixCmdConfig_target cfg
102 path = _target_path $ _nixCmdConfig_target cfg
@@ -124,7 +134,7 @@ instance HasNixCommonConfig NixBuildConfig where
124instance Default NixBuildConfig where
134instance Default NixBuildConfig where
125 def = NixBuildConfig def def
135 def = NixBuildConfig def def
126
136
127−runNixBuildConfig :: NixBuildConfig -> [FilePath]
137+runNixBuildConfig :: NixBuildConfig -> [String]
128runNixBuildConfig cfg = mconcat
138runNixBuildConfig cfg = mconcat
129 [ runNixCommonConfig $ cfg ^. nixCommonConfig
139 [ runNixCommonConfig $ cfg ^. nixCommonConfig
130 , case _nixBuildConfig_outLink cfg of
140 , case _nixBuildConfig_outLink cfg of
@@ -145,12 +155,26 @@ instance HasNixCommonConfig NixInstantiateConfig where
145instance Default NixInstantiateConfig where
155instance Default NixInstantiateConfig where
146 def = NixInstantiateConfig def False
156 def = NixInstantiateConfig def False
147
157
148−runNixInstantiateConfig :: NixInstantiateConfig -> [FilePath]
158+runNixInstantiateConfig :: NixInstantiateConfig -> [String]
149runNixInstantiateConfig cfg = mconcat
159runNixInstantiateConfig cfg = mconcat
150 [ runNixCommonConfig $ cfg ^. nixCommonConfig
160 [ runNixCommonConfig $ cfg ^. nixCommonConfig
151 , "--eval" <$ guard (_nixInstantiateConfig_eval cfg)
161 , "--eval" <$ guard (_nixInstantiateConfig_eval cfg)
152 ]
162 ]
153
163
164+data NixShellConfig = NixShellConfig
165+ { _nixShellConfig_common :: NixCommonConfig
166+ , _nixShellConfig_pure :: Bool
167+ , _nixShellConfig_run :: Maybe String
168+ }
169+
170+makeLenses ''NixShellConfig
171+
172+instance HasNixCommonConfig NixShellConfig where
173+ nixCommonConfig = nixShellConfig_common
174+
175+instance Default NixShellConfig where
176+ def = NixShellConfig def False Nothing
177+
154data NixCmd
178data NixCmd
155 = NixCmd_Build NixBuildConfig
179 = NixCmd_Build NixBuildConfig
156 | NixCmd_Instantiate NixInstantiateConfig
180 | NixCmd_Instantiate NixInstantiateConfig
@@ -158,27 +182,43 @@ data NixCmd
158instance Default NixCmd where
182instance Default NixCmd where
159 def = NixCmd_Build def
183 def = NixCmd_Build def
160
184
161−nixCmd :: MonadObelisk m => NixCmd -> m FilePath
185+runNixShellConfig :: NixShellConfig -> [String]
162−nixCmd cmdCfg = withSpinner' ("Running " <> cmd <> desc) (Just $ const $ "Built " <> desc) $ do
186+runNixShellConfig cfg = mconcat
187+ [ runNixCommonConfig $ cfg ^. nixCommonConfig
188+ , [ "--pure" | cfg ^. nixShellConfig_pure ]
189+ ] ++ mconcat [
190+ ["--run", run] | run <- maybeToList $ cfg ^. nixShellConfig_run
191+ ]
192+
193+nixCmdProc :: NixCmd -> ProcessSpec
194+nixCmdProc = fst . nixCmdProc'
195+
196+nixCmdProc' :: NixCmd -> (ProcessSpec, T.Text)
163− output <- readProcessAndLogStderr Debug $ proc (T.unpack cmd) $ options
197+nixCmdProc' cmdCfg = (proc (T.unpack cmd) options, cmd)
164− -- Remove final newline that Nix appends
165− Just (outPath, '\n') <- pure $ T.unsnoc output
166− pure $ T.unpack outPath
167 where
198 where
168− (cmd, options, commonCfg) = case cmdCfg of
199+ (cmd, options) = case cmdCfg of
169 NixCmd_Build cfg' ->
200 NixCmd_Build cfg' ->
170 ( "nix-build"
201 ( "nix-build"
171 , runNixBuildConfig cfg'
202 , runNixBuildConfig cfg'
172− , cfg' ^. nixCommonConfig
173 )
203 )
174 NixCmd_Instantiate cfg' ->
204 NixCmd_Instantiate cfg' ->
175 ( "nix-instantiate"
205 ( "nix-instantiate"
176 , runNixInstantiateConfig cfg'
206 , runNixInstantiateConfig cfg'
177− , cfg' ^. nixCommonConfig
178 )
207 )
208+
209+nixCmd :: MonadObelisk m => NixCmd -> m FilePath
210+nixCmd cmdCfg = withSpinner' ("Running " <> cmd <> desc) (Just $ const $ "Built " <> desc) $ do
211+ output <- readProcessAndLogStderr Debug cmdProc
212+ -- Remove final newline that Nix appends
213+ Just (outPath, '\n') <- pure $ T.unsnoc output
214+ pure $ T.unpack outPath
215+ where
216+ (cmdProc, cmd) = nixCmdProc' cmdCfg
217+ commonCfg = case cmdCfg of
218+ NixCmd_Build cfg' -> cfg' ^. nixCommonConfig
219+ NixCmd_Instantiate cfg' -> cfg' ^. nixCommonConfig
179 path = commonCfg ^. nixCmdConfig_target . target_path
220 path = commonCfg ^. nixCmdConfig_target . target_path
180 desc = T.pack $ mconcat $ catMaybes
221 desc = T.pack $ mconcat $ catMaybes
181− [ (" on " <>) <$> path
222+ [ ("on " <>) <$> path
182 , (\a -> " [" <> a <> "]") <$> (commonCfg ^. nixCmdConfig_target . target_attr)
223 , (\a -> " [" <> a <> "]") <$> (commonCfg ^. nixCmdConfig_target . target_attr)
183 ]
224 ]
184−
lib/command/src/Obelisk/Command/Preprocessor.hsnew file+96
@@ -0,0 +1,96 @@
1+{-# LANGUAGE LambdaCase #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+
4+module Obelisk.Command.Preprocessor where
5+
6+import qualified Data.ByteString.Lazy as BL
7+import qualified Data.ByteString.Lazy.Builder as BU
8+import Data.List (intersperse, isPrefixOf, sortOn)
9+import Data.Maybe (fromMaybe)
10+import qualified Data.Text.Lazy.Builder as TL
11+import qualified Data.Text.Lazy.Encoding as TL
12+import Distribution.Compiler (CompilerFlavor (..))
13+import Language.Haskell.Extension (Extension (..), Language(..))
14+import System.IO (IOMode (..), hClose, hPutStrLn, openFile, stderr)
15+import System.FilePath (hasTrailingPathSeparator, joinPath, normalise, splitPath)
16+
17+import Obelisk.Command.Run (CabalPackageInfo (..), parseCabalPackage')
18+
19+applyPackages :: FilePath -> FilePath -> FilePath -> [FilePath] -> IO ()
20+applyPackages origPath inPath outPath packagePaths = do
21+ -- This code is intended to be executed via ghci's -pgmF preprocessor option
22+ -- The command line arguments are passed in via ghc, which dictates the first three options and meanings
23+ -- In order for this code to execute, origPath must contain either a '.' character or a '/' character.
24+ -- (This is to avoid the possibility of the command line syntax conflicting with another ob command)
25+ -- We do have control over the remaining arguments, but they must be the same for all files.
26+ -- Thus, the fourth command line argument must be "apply-packages", which has already been handled.
27+ -- We assume all the remaining arguments passed in are paths to cabal or hpack package specifications.
28+
29+ outFile <- openFile outPath WriteMode
30+ let hPutTextBuilder h = BU.hPutBuilder h . TL.encodeUtf8Builder . TL.toLazyText
31+
32+ -- putStr "--------------------------------------------------------------------------------\n"
33+ -- print args
34+ -- putStr "--------------------------------------------------------------------------------\n"
35+
36+ -- Thus we must select among the packagePaths for the file we are going to parse.
37+
38+ let takeDirs = takeWhile hasTrailingPathSeparator
39+ packageDirs = sortOn (negate . length . takeDirs) $ map (splitPath . normalise) packagePaths
40+ origDir = splitPath $ normalise origPath
41+ matches = [ joinPath d | d <- packageDirs, takeDirs d `isPrefixOf` origDir ]
42+
43+ -- So the first element of matches is going to be the deepest path to a package spec that contains
44+ -- our file as a subdirectory.
45+
46+ case matches of
47+ [] -> hPutTextBuilder outFile (lineNumberPragma origPath) -- TODO: probably should produce a warning
48+ (packagePath:_) -> do
49+ parseCabalPackage' packagePath >>= \case
50+ Left err ->
51+ hPutStrLn stderr $ "Error: Unable to parse cabal package " <> packagePath <> "; Skipping preprocessor on " <> origPath <> ". Error was " <> show err
52+ Right (_warnings, packageInfo) ->
53+ hPutTextBuilder outFile (generateHeader origPath packageInfo)
54+
55+ BL.readFile inPath >>= BL.hPut outFile
56+ hClose outFile
57+
58+
59+ -- I'm pretty sure there's a certain amount of oversimplification in CabalPackageInfo, so I doubt this is fully robust.
60+
61+generateHeader :: FilePath -> CabalPackageInfo -> TL.Builder
62+generateHeader origPath packageInfo =
63+ hsExtensions <> ghcOptions <> lineNumberPragma origPath
64+ where
65+ hsExtensions =
66+ if not (null extList)
67+ then TL.fromText "{-# LANGUAGE "
68+ <> mconcat (intersperse (TL.fromText ", ") extList)
69+ <> TL.fromText " #-}\n"
70+ else mempty
71+ extList = addDefaultLanguage $ concatMap showExt $ _cabalPackageInfo_defaultExtensions packageInfo
72+ addDefaultLanguage =
73+ case _cabalPackageInfo_defaultLanguage packageInfo of
74+ Nothing -> id
75+ Just x -> case x of
76+ UnknownLanguage ext -> ( TL.fromString ext :)
77+ ext -> ( TL.fromString (show ext) :)
78+ showExt = \case
79+ EnableExtension ext -> [TL.fromString (show ext)]
80+ DisableExtension _ -> []
81+ UnknownExtension ext -> [TL.fromString ext]
82+
83+ ghcOptions =
84+ if not (null optList)
85+ then TL.fromText "{-# OPTIONS_GHC "
86+ <> mconcat (intersperse (TL.fromText " ") optList)
87+ <> TL.fromText " #-}\n"
88+ else mempty
89+ optList = map TL.fromString
90+ $ filter (\x -> not (isPrefixOf "-O" x))
91+ $ fromMaybe []
92+ $ lookup GHC (_cabalPackageInfo_compilerOptions packageInfo)
93+
94+lineNumberPragma :: FilePath -> TL.Builder
95+lineNumberPragma origPath =
96+ TL.fromText "{-# LINE 1 \"" <> TL.fromString origPath <> TL.fromText "\" #-}\n"
lib/command/src/Obelisk/Command/Project.hs+164−54
@@ -3,37 +3,55 @@
3{-# LANGUAGE OverloadedStrings #-}
3{-# LANGUAGE OverloadedStrings #-}
4module Obelisk.Command.Project
4module Obelisk.Command.Project
5 ( InitSource (..)
5 ( InitSource (..)
6− , initProject
7 , findProjectObeliskCommand
6 , findProjectObeliskCommand
8 , findProjectRoot
7 , findProjectRoot
9− , withProjectRoot
8+ , findProjectAssets
10− , inProjectShell
9+ , initProject
11− , inImpureProjectShell
10+ , nixShellRunConfig
11+ , nixShellRunProc
12+ , nixShellWithHoogle
13+ , nixShellWithPkgs
14+ , obeliskDirName
12 , projectShell
15 , projectShell
13−
14− , toObeliskDir
15 , toImplDir
16 , toImplDir
17+ , toNixPath
18+ , toObeliskDir
19+ , withProjectRoot
16 ) where
20 ) where
17
21
22+import Control.Concurrent.MVar (MVar, newMVar, withMVarMasked)
23+import Control.Lens ((.~), (?~), (<&>))
18import Control.Monad
24import Control.Monad
19import Control.Monad.IO.Class (liftIO)
25import Control.Monad.IO.Class (liftIO)
20import Control.Monad.State
26import Control.Monad.State
27+import qualified Data.Aeson as Json
21import Data.Bits
28import Data.Bits
29+import qualified Data.ByteString.Lazy as BSL
30+import Data.Default (def)
22−import Data.Function (on)
31+import Data.Function ((&), on)
32+import Data.List (isInfixOf)
33+import Data.Map (Map)
34+import Data.Text (Text)
23import qualified Data.Text as T
35import qualified Data.Text as T
36+import Data.Text.Encoding (decodeUtf8)
37+import Data.Traversable (for)
24import System.Directory
38import System.Directory
39+import System.Environment (lookupEnv)
25import System.FilePath
40import System.FilePath
26import System.IO.Temp
41import System.IO.Temp
42+import System.IO.Unsafe (unsafePerformIO)
27−import System.Posix (FileStatus, UserID, deviceID, fileID, fileMode, fileOwner, getFileStatus, getRealUserID)
43+import System.Posix (FileStatus, FileMode, CMode (..), UserID, deviceID, fileID, fileMode, fileOwner, getFileStatus, getRealUserID)
28import System.Posix.Files
44import System.Posix.Files
29−import System.Process (CreateProcess, cwd, proc, waitForProcess, delegate_ctlc)
30
45
31import GitHub.Data.GitData (Branch)
46import GitHub.Data.GitData (Branch)
32import GitHub.Data.Name (Name)
47import GitHub.Data.Name (Name)
33
48
34import Obelisk.App (MonadObelisk)
49import Obelisk.App (MonadObelisk)
35import Obelisk.CliApp
50import Obelisk.CliApp
51+import Obelisk.Command.Nix
36import Obelisk.Command.Thunk
52import Obelisk.Command.Thunk
53+import Obelisk.Command.Utils (nixExePath, nixBuildExePath)
54+
37--TODO: Make this module resilient to random exceptions
55--TODO: Make this module resilient to random exceptions
38
56
39--TODO: Don't hardcode this
57--TODO: Don't hardcode this
@@ -47,17 +65,21 @@ obeliskSourceWithBranch branch = ThunkSource_GitHub $ GitHubSource
47 { _gitHubSource_owner = "obsidiansystems"
65 { _gitHubSource_owner = "obsidiansystems"
48 , _gitHubSource_repo = "obelisk"
66 , _gitHubSource_repo = "obelisk"
49 , _gitHubSource_branch = Just branch
67 , _gitHubSource_branch = Just branch
68+ , _gitHubSource_private = False
50 }
69 }
51
70
52data InitSource
71data InitSource
53− = InitSource_Default
72+ = InitSource_Default
54− | InitSource_Branch (Name Branch)
73+ | InitSource_Branch (Name Branch)
55− | InitSource_Symlink FilePath
74+ | InitSource_Symlink FilePath
56− deriving Show
75+ deriving Show
76+
77+obeliskDirName :: FilePath
78+obeliskDirName = ".obelisk"
57
79
58-- | Path to obelisk directory in given path
80-- | Path to obelisk directory in given path
59toObeliskDir :: FilePath -> FilePath
81toObeliskDir :: FilePath -> FilePath
60−toObeliskDir p = p </> ".obelisk"
82+toObeliskDir p = p </> obeliskDirName
61
83
62-- | Path to impl file in given path
84-- | Path to impl file in given path
63toImplDir :: FilePath -> FilePath
85toImplDir :: FilePath -> FilePath
@@ -114,15 +136,16 @@ initProject source force = withSystemTempDirectory "ob-init" $ \tmpDir -> do
114findProjectObeliskCommand :: MonadObelisk m => FilePath -> m (Maybe FilePath)
136findProjectObeliskCommand :: MonadObelisk m => FilePath -> m (Maybe FilePath)
115findProjectObeliskCommand target = do
137findProjectObeliskCommand target = do
116 myUid <- liftIO getRealUserID
138 myUid <- liftIO getRealUserID
139+ processUmask <- liftIO getUmask
117 targetStat <- liftIO $ getFileStatus target
140 targetStat <- liftIO $ getFileStatus target
118− (result, insecurePaths) <- flip runStateT [] $ walkToProjectRoot target targetStat myUid >>= \case
141+ (result, insecurePaths) <- flip runStateT [] $ walkToProjectRoot target targetStat processUmask myUid >>= \case
119 Nothing -> pure Nothing
142 Nothing -> pure Nothing
120 Just projectRoot -> liftIO (doesDirectoryExist $ toImplDir projectRoot) >>= \case
143 Just projectRoot -> liftIO (doesDirectoryExist $ toImplDir projectRoot) >>= \case
121 False -> do
144 False -> do
122 putLog Warning $ "Found obelisk directory in " <> T.pack projectRoot <> " but the implementation (impl) file is missing"
145 putLog Warning $ "Found obelisk directory in " <> T.pack projectRoot <> " but the implementation (impl) file is missing"
123 pure Nothing
146 pure Nothing
124 True -> do
147 True -> do
125− walkToImplDir projectRoot myUid -- For security check
148+ walkToImplDir projectRoot myUid processUmask -- For security check
126 return $ Just projectRoot
149 return $ Just projectRoot
127 case (result, insecurePaths) of
150 case (result, insecurePaths) of
128 (Just projDir, []) -> do
151 (Just projDir, []) -> do
@@ -133,17 +156,37 @@ findProjectObeliskCommand target = do
133 putLog Error $ T.unlines
156 putLog Error $ T.unlines
134 [ "Error: Found a project at " <> T.pack (normalise projDir) <> ", but had to traverse one or more insecure directories to get there:"
157 [ "Error: Found a project at " <> T.pack (normalise projDir) <> ", but had to traverse one or more insecure directories to get there:"
135 , T.unlines $ fmap (T.pack . normalise) insecurePaths
158 , T.unlines $ fmap (T.pack . normalise) insecurePaths
136− , "Please ensure that all of these directories are owned by you and are not writable by anyone else."
159+ , "Please ensure that all of these directories are owned by you, not world-writable, and no more group-writable than permitted by your umask."
137 ]
160 ]
138 return Nothing
161 return Nothing
139
162
163+-- | Get the umask for the Obelisk process.
164+--
165+-- Because of
166+-- http://man7.org/linux/man-pages/man2/umask.2.html#NOTES we have to set the
167+-- umask to read it. We are using 'withMVarMasked' to guarantee that setting and
168+-- reading isn't interrupted by any exception or interleaved with another thread.
169+getUmask :: IO FileMode
170+getUmask = withMVarMasked globalUmaskLock $ \() -> do
171+ initialMask <- setFileCreationMask safeUmask
172+ void (setFileCreationMask initialMask)
173+ pure initialMask
174+ where
175+ safeUmask :: FileMode
176+ safeUmask = CMode 0o22
177+
178+{-# NOINLINE globalUmaskLock #-}
179+globalUmaskLock :: MVar ()
180+globalUmaskLock = unsafePerformIO (newMVar ())
181+
140-- | Get the FilePath to the containing project directory, if there is one
182-- | Get the FilePath to the containing project directory, if there is one
141findProjectRoot :: MonadObelisk m => FilePath -> m (Maybe FilePath)
183findProjectRoot :: MonadObelisk m => FilePath -> m (Maybe FilePath)
142findProjectRoot target = do
184findProjectRoot target = do
143 myUid <- liftIO getRealUserID
185 myUid <- liftIO getRealUserID
144 targetStat <- liftIO $ getFileStatus target
186 targetStat <- liftIO $ getFileStatus target
187+ umask <- liftIO getUmask
145− (result, _) <- liftIO $ runStateT (walkToProjectRoot target targetStat myUid) []
188+ (result, _) <- liftIO $ runStateT (walkToProjectRoot target targetStat umask myUid) []
146− return result
189+ return $ makeRelative "." <$> result
147
190
148withProjectRoot :: MonadObelisk m => FilePath -> (FilePath -> m a) -> m a
191withProjectRoot :: MonadObelisk m => FilePath -> (FilePath -> m a) -> m a
149withProjectRoot target f = findProjectRoot target >>= \case
192withProjectRoot target f = findProjectRoot target >>= \case
@@ -155,15 +198,15 @@ withProjectRoot target f = findProjectRoot target >>= \case
155-- traversed in the process. Return the project root directory, if found.
198-- traversed in the process. Return the project root directory, if found.
156walkToProjectRoot
199walkToProjectRoot
157 :: (MonadState [FilePath] m, MonadIO m)
200 :: (MonadState [FilePath] m, MonadIO m)
158− => FilePath -> FileStatus -> UserID -> m (Maybe FilePath)
201+ => FilePath -> FileStatus -> FileMode -> UserID -> m (Maybe FilePath)
159−walkToProjectRoot this thisStat myUid = liftIO (doesDirectoryExist this) >>= \case
202+walkToProjectRoot this thisStat desiredUmask myUid = liftIO (doesDirectoryExist this) >>= \case
160 -- It's not a directory, so it can't be a project
203 -- It's not a directory, so it can't be a project
161 False -> do
204 False -> do
162 let dir = takeDirectory this
205 let dir = takeDirectory this
163 dirStat <- liftIO $ getFileStatus dir
206 dirStat <- liftIO $ getFileStatus dir
164− walkToProjectRoot dir dirStat myUid
207+ walkToProjectRoot dir dirStat desiredUmask myUid
165 True -> do
208 True -> do
166− when (not $ isWritableOnlyBy thisStat myUid) $ modify (this:)
209+ when (not $ isWellOwnedAndWellPermissioned thisStat myUid desiredUmask) $ modify (this:)
167 liftIO (doesDirectoryExist $ toObeliskDir this) >>= \case
210 liftIO (doesDirectoryExist $ toObeliskDir this) >>= \case
168 True -> return $ Just this
211 True -> return $ Just this
169 False -> do
212 False -> do
@@ -173,47 +216,114 @@ walkToProjectRoot this thisStat myUid = liftIO (doesDirectoryExist this) >>= \ca
173 isSameFileAs = (==) `on` fileIdentity
216 isSameFileAs = (==) `on` fileIdentity
174 if thisStat `isSameFileAs` nextStat
217 if thisStat `isSameFileAs` nextStat
175 then return Nothing -- Found a cycle; probably hit root directory
218 then return Nothing -- Found a cycle; probably hit root directory
176− else walkToProjectRoot next nextStat myUid
219+ else walkToProjectRoot next nextStat desiredUmask myUid
177
220
178-- | Walk from the given project root directory to its Obelisk implementation
221-- | Walk from the given project root directory to its Obelisk implementation
179-- directory, accumulating potentially insecure directories that were traversed
222-- directory, accumulating potentially insecure directories that were traversed
180-- in the process.
223-- in the process.
181−walkToImplDir :: (MonadState [FilePath] m, MonadIO m) => FilePath -> UserID -> m ()
224+walkToImplDir :: (MonadState [FilePath] m, MonadIO m) => FilePath -> UserID -> FileMode -> m ()
182−walkToImplDir projectRoot myUid = do
225+walkToImplDir projectRoot myUid umask = do
183 let obDir = toObeliskDir projectRoot
226 let obDir = toObeliskDir projectRoot
184 obDirStat <- liftIO $ getFileStatus obDir
227 obDirStat <- liftIO $ getFileStatus obDir
185− when (not $ isWritableOnlyBy obDirStat myUid) $ modify (obDir:)
228+ when (not $ isWellOwnedAndWellPermissioned obDirStat myUid umask) $ modify (obDir:)
186 let implThunk = obDir </> "impl"
229 let implThunk = obDir </> "impl"
187 implThunkStat <- liftIO $ getFileStatus implThunk
230 implThunkStat <- liftIO $ getFileStatus implThunk
188− when (not $ isWritableOnlyBy implThunkStat myUid) $ modify (implThunk:)
231+ when (not $ isWellOwnedAndWellPermissioned implThunkStat myUid umask) $ modify (implThunk:)
232+
233+-- | Check to see if directory is writable by a user whose User ID matches the
234+-- second argument provided, and if the fact that other people can write to that
235+-- directory is in accordance with the umask of the system, passed as the third
236+-- argument.
237+isWellOwnedAndWellPermissioned :: FileStatus -> UserID -> FileMode -> Bool
238+isWellOwnedAndWellPermissioned s uid umask = isOwnedBy s uid && filePermissionIsSafe s umask
239+
240+isOwnedBy :: FileStatus -> UserID -> Bool
241+isOwnedBy s uid = fileOwner s == uid
189
242
190−--TODO: Is there a better way to ask if anyone else can write things?
243+-- | Check to see if a directory respect the umask, but check explicitly that
191−--E.g. what about ACLs?
244+-- it's not world writable in any case.
192−-- | Check to see if directory is only writable by a user whose User ID matches the second argument provided
245+filePermissionIsSafe :: FileStatus -> FileMode -> Bool
193−isWritableOnlyBy :: FileStatus -> UserID -> Bool
246+filePermissionIsSafe s umask = not fileWorldWritable && fileGroupWritable <= umaskGroupWritable
194−isWritableOnlyBy s uid = fileOwner s == uid && fileMode s .&. 0o22 == 0
247+ where
248+ fileWorldWritable = fileMode s .&. 0o002 == 0o002
249+ fileGroupWritable = fileMode s .&. 0o020 == 0o020
250+ umaskGroupWritable = umask .&. 0o020 == 0
195
251
196−-- | Run a command in the given shell for the current project
252+-- | Nix syntax requires relative paths to be prefixed by @./@ or
197−inProjectShell :: MonadObelisk m => String -> String -> m ()
253+-- @../@. This will make a 'FilePath' that can be embedded in a Nix
198−inProjectShell shellName command = withProjectRoot "." $ \root ->
254+-- expression.
199− projectShell root True shellName command
255+toNixPath :: FilePath -> FilePath
256+toNixPath root | "/" `isInfixOf` root = root
257+ | otherwise = "./" <> root
200
258
201−inImpureProjectShell :: MonadObelisk m => String -> String -> m ()
259+nixShellRunConfig :: MonadObelisk m => FilePath -> Bool -> Maybe String -> m NixShellConfig
202−inImpureProjectShell shellName command = withProjectRoot "." $ \root ->
260+nixShellRunConfig root isPure command = do
203− projectShell root False shellName command
261+ nixpkgsPath <- fmap T.strip $ readProcessAndLogStderr Debug $ setCwd (Just root) $
262+ proc nixExePath ["eval", "(import .obelisk/impl {}).nixpkgs.path"]
263+ nixRemote <- liftIO $ lookupEnv "NIX_REMOTE"
264+ pure $ def
265+ & nixShellConfig_pure .~ isPure
266+ & nixShellConfig_common . nixCmdConfig_target .~ (def & target_path .~ Nothing)
267+ & nixShellConfig_run .~ (command <&> \c -> mconcat
268+ [ "export NIX_PATH=nixpkgs=", T.unpack nixpkgsPath, "; "
269+ , maybe "" (\v -> "export NIX_REMOTE=" <> v <> "; ") nixRemote
270+ , c
271+ ])
204
272
273+nixShellRunProc :: NixShellConfig -> ProcessSpec
274+nixShellRunProc cfg = setDelegateCtlc True $ proc "nix-shell" $ runNixShellConfig cfg
275+
276+nixShellWithPkgs :: MonadObelisk m => FilePath -> Bool -> Bool -> Map Text FilePath -> Maybe String -> m ()
277+nixShellWithPkgs root isPure chdirToRoot packageNamesAndPaths command = do
278+ packageNamesAndAbsPaths <- liftIO $ for packageNamesAndPaths makeAbsolute
279+ defShellConfig <- nixShellRunConfig root isPure command
280+ let setCwd_ = if chdirToRoot then setCwd (Just root) else id
281+ (_, _, _, ph) <- createProcess_ "nixShellWithPkgs" $ setCwd_ $ nixShellRunProc $ defShellConfig
282+ & nixShellConfig_common . nixCmdConfig_target . target_expr ?~
283+ "{root, pkgs}: ((import root {}).passthru.__unstable__.self.extend (_: _: {\
284+ \shellPackages = builtins.fromJSON pkgs;\
285+ \})).project.shells.ghc"
286+ & nixShellConfig_common . nixCmdConfig_args .~
287+ [ rawArg "root" $ toNixPath $ if chdirToRoot then "." else root
288+ , strArg "pkgs" (T.unpack $ decodeUtf8 $ BSL.toStrict $ Json.encode packageNamesAndAbsPaths)
289+ ]
290+ void $ waitForProcess ph
291+
292+nixShellWithHoogle :: MonadObelisk m => FilePath -> Bool -> String -> Maybe String -> m ()
293+nixShellWithHoogle root isPure shell' command = do
294+ defShellConfig <- nixShellRunConfig root isPure command
295+ (_, _, _, ph) <- createProcess_ "nixShellWithHoogle" $ setCwd (Just root) $ nixShellRunProc $ defShellConfig
296+ & nixShellConfig_common . nixCmdConfig_target . target_expr ?~
297+ "{shell}: ((import ./. {}).passthru.__unstable__.self.extend (_: super: {\
298+ \userSettings = super.userSettings // { withHoogle = true; };\
299+ \})).project.shells.${shell}"
300+ & nixShellConfig_common . nixCmdConfig_args .~ [ strArg "shell" shell' ]
301+ void $ waitForProcess ph
302+
205−projectShell :: MonadObelisk m => FilePath -> Bool -> String -> String -> m ()
303+projectShell :: MonadObelisk m => FilePath -> Bool -> String -> Maybe String -> m ()
206projectShell root isPure shellName command = do
304projectShell root isPure shellName command = do
305+ defShellConfig <- nixShellRunConfig root isPure command
207− (_, _, _, ph) <- createProcess_ "runNixShellAttr" $ setCtlc $ setCwd (Just root) $ proc "nix-shell" $
306+ (_, _, _, ph) <- createProcess_ "runNixShellAttr" $ setCwd (Just root) $ nixShellRunProc $ defShellConfig
208− [ "--pure" | isPure ] <>
307+ & nixShellConfig_common . nixCmdConfig_target . target_path ?~ "default.nix"
209− [ "-A"
308+ & nixShellConfig_common . nixCmdConfig_target . target_attr ?~ ("shells." <> shellName)
210− , "shells." <> shellName
211− , "--run", command
212− ]
213− void $ liftIO $ waitForProcess ph
309+ void $ waitForProcess ph
214−
310+
215−setCtlc :: CreateProcess -> CreateProcess
311+findProjectAssets :: MonadObelisk m => FilePath -> m Text
216−setCtlc cfg = cfg { delegate_ctlc = True }
312+findProjectAssets root = do
217−
313+ isDerivation <- readProcessAndLogStderr Debug $ setCwd (Just root) $
218−setCwd :: Maybe FilePath -> CreateProcess -> CreateProcess
314+ proc nixExePath
219−setCwd fp cfg = cfg { cwd = fp }
315+ [ "eval"
316+ , "(let a = import ./. {}; in toString (a.reflex.nixpkgs.lib.isDerivation a.passthru.staticFilesImpure))"
317+ , "--raw"
318+ -- `--raw` is not available with old nix-instantiate. It drops quotation
319+ -- marks and trailing newline, so is very convenient for shelling out.
320+ ]
321+ -- Check whether the impure static files are a derivation (and so must be built)
322+ if isDerivation == "1"
323+ then fmap T.strip $ readProcessAndLogStderr Debug $ setCwd (Just root) $ -- Strip whitespace here because nix-build has no --raw option
324+ proc nixBuildExePath
325+ [ "--no-out-link"
326+ , "-E", "(import ./. {}).passthru.staticFilesImpure"
327+ ]
328+ else readProcessAndLogStderr Debug $ setCwd (Just root) $
329+ proc nixExePath ["eval", "-f", ".", "passthru.staticFilesImpure", "--raw"]
lib/command/src/Obelisk/Command/Run.hs+307−121
@@ -3,164 +3,329 @@
3{-# LANGUAGE MultiParamTypeClasses #-}
3{-# LANGUAGE MultiParamTypeClasses #-}
4{-# LANGUAGE OverloadedStrings #-}
4{-# LANGUAGE OverloadedStrings #-}
5{-# LANGUAGE ScopedTypeVariables #-}
5{-# LANGUAGE ScopedTypeVariables #-}
6+{-# LANGUAGE TupleSections #-}
7+{-# LANGUAGE QuasiQuotes #-}
6module Obelisk.Command.Run where
8module Obelisk.Command.Run where
7
9
8−import Control.Exception (bracket)
10+import Control.Arrow ((&&&))
11+import Control.Exception (Exception, bracket)
9−import Control.Monad
12+import Control.Lens (ifor, (.~), (&))
13+import Control.Monad (filterM, unless, void)
14+import Control.Monad.Except (runExceptT, throwError)
10import Control.Monad.IO.Class (liftIO)
15import Control.Monad.IO.Class (liftIO)
11import Control.Monad.Reader (MonadIO)
16import Control.Monad.Reader (MonadIO)
17+import Data.Coerce (coerce)
18+import Data.Default (def)
12import Data.Either
19import Data.Either
13−import Data.List
20+import Data.Foldable (for_, toList)
14−import Data.List.NonEmpty as NE
21+import qualified Data.List.NonEmpty as NE
22+import Data.Map (Map)
23+import qualified Data.Map as Map
15import Data.Maybe
24import Data.Maybe
25+import qualified Data.Set as Set
26+import Data.Text (Text)
16import qualified Data.Text as T
27import qualified Data.Text as T
28+import Data.Time.Clock (getCurrentTime)
29+import Data.Time.Format (formatTime, defaultTimeLocale)
30+import Data.Traversable (for)
31+import Debug.Trace (trace)
32+import Distribution.Compiler (CompilerFlavor(..))
17import Distribution.PackageDescription.Parsec (parseGenericPackageDescription)
33import Distribution.PackageDescription.Parsec (parseGenericPackageDescription)
18import Distribution.Parsec.ParseResult (runParseResult)
34import Distribution.Parsec.ParseResult (runParseResult)
19−import Distribution.Pretty
35+import qualified Distribution.System as Dist
20import Distribution.Types.BuildInfo
36import Distribution.Types.BuildInfo
21import Distribution.Types.CondTree
37import Distribution.Types.CondTree
22import Distribution.Types.GenericPackageDescription
38import Distribution.Types.GenericPackageDescription
23import Distribution.Types.Library
39import Distribution.Types.Library
24import Distribution.Utils.Generic
40import Distribution.Utils.Generic
41+import qualified Distribution.Parsec.Common as Dist
25−import Hpack.Config
42+import qualified Hpack.Config as Hpack
26−import Hpack.Render
43+import qualified Hpack.Render as Hpack
27−import Hpack.Yaml
44+import qualified Hpack.Yaml as Hpack
28import Language.Haskell.Extension
45import Language.Haskell.Extension
29import Network.Socket hiding (Debug)
46import Network.Socket hiding (Debug)
30import System.Directory
47import System.Directory
48+import System.Environment (getExecutablePath)
31import System.FilePath
49import System.FilePath
32−import System.Process (proc)
50+import qualified System.Info
33import System.IO.Temp (withSystemTempDirectory)
51import System.IO.Temp (withSystemTempDirectory)
34−import Data.ByteString (ByteString)
35
52
36−import Obelisk.App (MonadObelisk, ObeliskT)
53+import Obelisk.App (MonadObelisk)
37−import Obelisk.CliApp
54+import Obelisk.CliApp (
38− ( CliT (..), HasCliConfig, Severity (..)
55+ Severity (..),
39− , callCommand, failWith, getCliConfig, putLog
56+ createProcess_,
57+ failWith,
58+ proc,
59+ putLog,
60+ readCreateProcessWithExitCode,
40− , readProcessAndLogStderr, runCli)
61+ readProcessAndLogStderr,
62+ setCwd,
63+ setDelegateCtlc,
64+ waitForProcess,
65+ withSpinner,
66+ )
67+import Obelisk.Command.Nix
41−import Obelisk.Command.Project (inProjectShell, withProjectRoot)
68+import Obelisk.Command.Project (nixShellWithPkgs, toImplDir, withProjectRoot, findProjectAssets)
69+import Obelisk.Command.Thunk (attrCacheFileName)
70+import Obelisk.Command.Utils (findExePath, ghcidExePath)
42
71
43data CabalPackageInfo = CabalPackageInfo
72data CabalPackageInfo = CabalPackageInfo
73+ { _cabalPackageInfo_packageFile :: FilePath
74+ , _cabalPackageInfo_packageName :: T.Text
44− { _cabalPackageInfo_packageRoot :: FilePath
75+ , _cabalPackageInfo_packageRoot :: FilePath
76+ , _cabalPackageInfo_buildable :: Bool
45 , _cabalPackageInfo_sourceDirs :: NE.NonEmpty FilePath
77 , _cabalPackageInfo_sourceDirs :: NE.NonEmpty FilePath
46 -- ^ List of hs src dirs of the library component
78 -- ^ List of hs src dirs of the library component
47 , _cabalPackageInfo_defaultExtensions :: [Extension]
79 , _cabalPackageInfo_defaultExtensions :: [Extension]
48 -- ^ List of globally enable extensions of the library component
80 -- ^ List of globally enable extensions of the library component
81+ , _cabalPackageInfo_defaultLanguage :: Maybe Language
82+ -- ^ List of globally set languages of the library component
83+ , _cabalPackageInfo_compilerOptions :: [(CompilerFlavor, [String])]
84+ -- ^ List of compiler-specific options (e.g., the "ghc-options" field of the cabal file)
49 }
85 }
50
86
51−-- NOTE: `run` is not polymorphic like the rest because we use StaticPtr to invoke it.
87+-- | Used to signal to obelisk that it's being invoked as a preprocessor
52−run :: ObeliskT IO ()
88+preprocessorIdentifier :: String
53−run = do
89+preprocessorIdentifier = "__preprocessor-apply-packages"
90+
91+profile
92+ :: MonadObelisk m
93+ => String
94+ -> [String]
95+ -> m ()
96+profile profileBasePattern rtsFlags = withProjectRoot "." $ \root -> do
97+ putLog Debug "Using profiled build of project."
98+
99+ outPath <- withSpinner "Building profiled executable" $
100+ fmap (T.unpack . T.strip) $ readProcessAndLogStderr Debug $ setCwd (Just root) $ nixCmdProc $
101+ NixCmd_Build $ def
102+ & nixBuildConfig_outLink .~ OutLink_None
103+ & nixCmdConfig_target .~ Target
104+ { _target_path = Just "."
105+ , _target_attr = Just "__unstable__.profiledObRun"
106+ , _target_expr = Nothing
107+ }
108+ assets <- findProjectAssets root
109+ putLog Debug $ "Assets impurely loaded from: " <> assets
110+ time <- liftIO getCurrentTime
111+ let profileBaseName = formatTime defaultTimeLocale profileBasePattern time
112+ liftIO $ createDirectoryIfMissing True $ takeDirectory $ root </> profileBaseName
113+ putLog Debug $ "Storing profiled data under base name of " <> T.pack (root </> profileBaseName)
114+ freePort <- getFreePort
115+ (_, _, _, ph) <- createProcess_ "runProfExe" $ setCwd (Just root) $ setDelegateCtlc True $ proc (outPath </> "bin" </> "ob-run") $
116+ [ show freePort
117+ , T.unpack assets
118+ , profileBaseName
119+ , "+RTS"
120+ , "-po" <> profileBaseName
121+ ] <> rtsFlags
122+ <> [ "-RTS" ]
123+ void $ waitForProcess ph
124+
125+run
126+ :: MonadObelisk m
127+ => m ()
128+run = withProjectRoot "." $ \root -> do
54− pkgs <- getLocalPkgs
129+ pkgs <- fmap toList . parsePackagesOrFail =<< getLocalPkgs root
55− withGhciScript pkgs $ \dotGhciPath -> do
130+ withGhciScript pkgs root $ \dotGhciPath -> do
56 freePort <- getFreePort
131 freePort <- getFreePort
57− assets <- withProjectRoot "." $ \root ->
132+ assets <- findProjectAssets root
58− -- `--raw` is not available with old nix-instantiate. It drops quotation
59− -- marks and trailing newline, so is very convenient for shelling out.
60− readProcessAndLogStderr Debug $
61− proc "nix" ["eval", "-f", root, "passthru.staticFilesImpure", "--raw"]
62 putLog Debug $ "Assets impurely loaded from: " <> assets
133 putLog Debug $ "Assets impurely loaded from: " <> assets
63− runGhcid dotGhciPath $ Just $ unwords
134+ runGhcid root True dotGhciPath pkgs $ Just $ unwords
64− [ "run"
135+ [ "Obelisk.Run.run"
65 , show freePort
136 , show freePort
66− , "(runServeAsset " ++ show assets ++ ")"
137+ , "(Obelisk.Run.runServeAsset " ++ show assets ++ ")"
67 , "Backend.backend"
138 , "Backend.backend"
68 , "Frontend.frontend"
139 , "Frontend.frontend"
69 ]
140 ]
70
141
71runRepl :: MonadObelisk m => m ()
142runRepl :: MonadObelisk m => m ()
72−runRepl = do
143+runRepl = withProjectRoot "." $ \root -> do
73− pkgs <- getLocalPkgs
144+ pkgs <- fmap toList . parsePackagesOrFail =<< getLocalPkgs root
74− withGhciScript pkgs $ \dotGhciPath -> do
145+ withGhciScript pkgs "." $ \dotGhciPath ->
75− runGhciRepl dotGhciPath
146+ runGhciRepl root pkgs dotGhciPath
76
147
77runWatch :: MonadObelisk m => m ()
148runWatch :: MonadObelisk m => m ()
78−runWatch = do
149+runWatch = withProjectRoot "." $ \root -> do
79− pkgs <- getLocalPkgs
150+ pkgs <- fmap toList . parsePackagesOrFail =<< getLocalPkgs root
80− withGhciScript pkgs $ \dotGhciPath -> runGhcid dotGhciPath Nothing
151+ withGhciScript pkgs root $ \dotGhciPath ->
152+ runGhcid root True dotGhciPath pkgs Nothing
81
153
82−-- | Relative paths to local packages of an obelisk project
154+-- | Relative paths to local packages of an obelisk project.
83−-- TODO a way to query this
155+--
156+-- These are a combination of the obelisk predefined local packages,
157+-- and any packages that the user has set with the @packages@ argument
158+-- to the Nix @project@ function.
84−getLocalPkgs :: Applicative f => f [FilePath]
159+getLocalPkgs :: MonadObelisk m => FilePath -> m [FilePath]
85−getLocalPkgs = pure ["backend", "common", "frontend"]
160+getLocalPkgs root = do
161+ obeliskPaths <- runFind ["-L", root, "-name", ".obelisk", "-type", "d"]
86
162
163+ -- We do not want to find packages that are embedded inside other obelisk projects, unless that
164+ -- obelisk project is our own.
165+ let exclusions = filter (/= root) $ map takeDirectory obeliskPaths
166+ fmap (map (makeRelative ".")) $ runFind $
167+ ["-L", root, "(", "-name", "*.cabal", "-o", "-name", Hpack.packageConfig, ")", "-a", "-type", "f"]
168+ <> concat [["-not", "-path", p </> "*"] | p <- (toImplDir "*" </> attrCacheFileName) : exclusions]
169+ where
170+ runFind args = do
171+ (_exitCode, out, err) <- readCreateProcessWithExitCode $ proc findExePath args
172+ putLog Debug $ T.strip $ T.pack err
173+ pure $ map T.unpack $ T.lines $ T.strip $ T.pack out
174+
175+data GuessPackageFileError = GuessPackageFileError_Ambiguous [FilePath] | GuessPackageFileError_NotFound
176+ deriving (Eq, Ord, Show)
177+instance Exception GuessPackageFileError
178+
179+newtype HPackFilePath = HPackFilePath { unHPackFilePath :: FilePath } deriving (Eq, Ord, Show)
180+newtype CabalFilePath = CabalFilePath { unCabalFilePath :: FilePath } deriving (Eq, Ord, Show)
181+
182+-- | Given a directory, try to guess what the appropriate @.cabal@ or @package.yaml@ file is for the package.
183+guessCabalPackageFile
184+ :: (MonadIO m)
185+ => FilePath -- ^ Directory or path to search for cabal package
186+ -> m (Either GuessPackageFileError (Either CabalFilePath HPackFilePath))
187+guessCabalPackageFile pkg = do
188+ liftIO (doesDirectoryExist pkg) >>= \case
189+ False -> case cabalOrHpackFile pkg of
190+ (Just hpack@(Right _)) -> pure $ Right hpack
191+ (Just cabal@(Left (CabalFilePath cabalFilePath))) -> do
192+ -- If the cabal file has a sibling hpack file, we use that instead
193+ -- since running hpack often generates a sibling cabal file
194+ let possibleHpackSibling = takeDirectory cabalFilePath </> Hpack.packageConfig
195+ hasHpackSibling <- liftIO $ doesFileExist possibleHpackSibling
196+ pure $ Right $ if hasHpackSibling then Right (HPackFilePath possibleHpackSibling) else cabal
197+ Nothing -> pure $ Left GuessPackageFileError_NotFound
198+ True -> do
199+ candidates <- liftIO $
200+ filterM (doesFileExist . either unCabalFilePath unHPackFilePath)
201+ =<< mapMaybe (cabalOrHpackFile . (pkg </>)) <$> listDirectory pkg
202+ pure $ case partitionEithers candidates of
203+ ([hpack], _) -> Right $ Left hpack
204+ ([], [cabal]) -> Right $ Right cabal
205+ ([], []) -> Left GuessPackageFileError_NotFound
206+ (hpacks, cabals) -> Left $ GuessPackageFileError_Ambiguous $ coerce hpacks <> coerce cabals
207+
208+cabalOrHpackFile :: FilePath -> Maybe (Either CabalFilePath HPackFilePath)
209+cabalOrHpackFile = \case
210+ x | takeExtension x == ".cabal" -> Just (Left $ CabalFilePath x)
211+ | takeFileName x == Hpack.packageConfig -> Just (Right $ HPackFilePath x)
212+ | otherwise -> Nothing
213+
214+-- | Parses the cabal package in a given directory.
215+-- This automatically figures out which .cabal file or package.yaml (hpack) file to use in the given directory.
87parseCabalPackage
216parseCabalPackage
88− :: (MonadObelisk m)
217+ :: MonadObelisk m
89− => FilePath -- ^ package directory
218+ => FilePath -- ^ Package directory
90 -> m (Maybe CabalPackageInfo)
219 -> m (Maybe CabalPackageInfo)
91−parseCabalPackage dir = do
220+parseCabalPackage dir = parseCabalPackage' dir >>= \case
92− let cabalFp = dir </> (takeBaseName dir <> ".cabal")
221+ Left err -> Nothing <$ putLog Error err
93− hpackFp = dir </> "package.yaml"
222+ Right (warnings, pkgInfo) -> do
94− hasCabal <- liftIO $ doesFileExist cabalFp
223+ for_ warnings $ putLog Warning . T.pack . show
95− hasHpack <- liftIO $ doesFileExist hpackFp
224+ pure $ Just pkgInfo
96−
225+
97− mCabalContents <- if hasCabal
226+-- | Like 'parseCabalPackage' but returns errors and warnings directly so as to avoid 'MonadObelisk'.
98− then Just <$> liftIO (readUTF8File cabalFp)
227+parseCabalPackage'
99− else if hasHpack
228+ :: (MonadIO m)
100− then do
229+ => FilePath -- ^ Package directory
101− let decodeOptions = DecodeOptions hpackFp Nothing decodeYaml
230+ -> m (Either T.Text ([Dist.PWarning], CabalPackageInfo))
102− liftIO (readPackageConfig decodeOptions) >>= \case
231+parseCabalPackage' pkg = runExceptT $ do
103− Left err -> do
232+ (cabalContents, packageFile, packageName) <- guessCabalPackageFile pkg >>= \case
104− putLog Error $ T.pack $ "Failed to parse " <> hpackFp <> ": " <> err
233+ Left GuessPackageFileError_NotFound -> throwError $ "No .cabal or package.yaml file found in " <> T.pack pkg
105− return Nothing
234+ Left (GuessPackageFileError_Ambiguous _) -> throwError $ "Unable to determine which .cabal file to use in " <> T.pack pkg
106− Right (DecodeResult hpackPackage _ _) -> do
235+ Right (Left (CabalFilePath file)) -> (, file, takeBaseName file) <$> liftIO (readUTF8File file)
107− return $ Just $ renderPackage [] hpackPackage
236+ Right (Right (HPackFilePath file)) -> do
108− else return Nothing
237+ let
109−
238+ decodeOptions = Hpack.DecodeOptions (Hpack.ProgramName "ob") file Nothing Hpack.decodeYaml
110− fmap join $ forM mCabalContents $ \cabalContents -> do
239+ liftIO (Hpack.readPackageConfig decodeOptions) >>= \case
111− let (warnings, result) = runParseResult $ parseGenericPackageDescription $
240+ Left err -> throwError $ T.pack $ "Failed to parse " <> file <> ": " <> err
112− toUTF8BS $ cabalContents
241+ Right (Hpack.DecodeResult hpackPackage _ _ _) -> pure (Hpack.renderPackage [] hpackPackage, file, Hpack.packageName hpackPackage)
113− mapM_ (putLog Warning) $ fmap (T.pack . show) warnings
242+
114− case result of
243+ let
115− Right gpkg -> do
244+ (warnings, result) = runParseResult $ parseGenericPackageDescription $ toUTF8BS cabalContents
116− return $ do
245+ osConfVar = case System.Info.os of
117− (_, lib) <- simplifyCondTree (const $ pure True) <$> condLibrary gpkg
246+ "linux" -> Just Dist.Linux
118− pure $ CabalPackageInfo
247+ "darwin" -> Just Dist.OSX
119− { _cabalPackageInfo_packageRoot = takeDirectory cabalFp
248+ _ -> trace "Unrecgonized System.Info.os" Nothing
120− , _cabalPackageInfo_sourceDirs =
249+ archConfVar = Just Dist.X86_64 -- TODO: Actually infer this
121− fromMaybe (pure ".") $ NE.nonEmpty $ hsSourceDirs $ libBuildInfo lib
250+ evalConfVar v = Right $ case v of
122− , _cabalPackageInfo_defaultExtensions =
251+ OS osVar -> Just osVar == osConfVar
123− defaultExtensions $ libBuildInfo lib
252+ Arch archVar -> Just archVar == archConfVar
124− }
253+ Impl GHC _ -> True -- TODO: Actually check version range
125− Left (_, errors) -> do
254+ _ -> False
126− putLog Error $ T.pack $ "Failed to parse " <> cabalFp <> ":"
255+ case condLibrary <$> result of
127− mapM_ (putLog Error) $ fmap (T.pack . show) errors
256+ Right (Just condLib) -> do
128− return Nothing
257+ let (_, lib) = simplifyCondTree evalConfVar condLib
129−
258+ pure $ (warnings,) $ CabalPackageInfo
130−withUTF8FileContentsM :: (MonadIO m, HasCliConfig e m) => FilePath -> (ByteString -> CliT e IO a) -> m a
259+ { _cabalPackageInfo_packageName = T.pack packageName
131−withUTF8FileContentsM fp f = do
260+ , _cabalPackageInfo_packageFile = packageFile
132− c <- getCliConfig
261+ , _cabalPackageInfo_packageRoot = takeDirectory packageFile
133− liftIO $ withUTF8FileContents fp $ runCli c . f . toUTF8BS
262+ , _cabalPackageInfo_buildable = buildable $ libBuildInfo lib
263+ , _cabalPackageInfo_sourceDirs =
264+ fromMaybe (pure ".") $ NE.nonEmpty $ hsSourceDirs $ libBuildInfo lib
265+ , _cabalPackageInfo_defaultExtensions =
266+ defaultExtensions $ libBuildInfo lib
267+ , _cabalPackageInfo_defaultLanguage =
268+ defaultLanguage $ libBuildInfo lib
269+ , _cabalPackageInfo_compilerOptions = options $ libBuildInfo lib
270+ }
271+ Right Nothing -> throwError "Haskell package has no library component"
272+ Left (_, errors) ->
273+ throwError $ T.pack $ "Failed to parse " <> packageFile <> ":\n" <> unlines (map show errors)
274+
275+parsePackagesOrFail :: MonadObelisk m => [FilePath] -> m (NE.NonEmpty CabalPackageInfo)
276+parsePackagesOrFail dirs = do
277+ (pkgDirErrs, packageInfos') <- fmap partitionEithers $ for dirs $ \dir -> do
278+ flip fmap (parseCabalPackage dir) $ \case
279+ Just packageInfo
280+ | _cabalPackageInfo_buildable packageInfo -> Right packageInfo
281+ _ -> Left dir
282+
283+ let packagesByName = Map.fromListWith (<>) [(_cabalPackageInfo_packageName p, p NE.:| []) | p <- packageInfos']
284+ unambiguous <- ifor packagesByName $ \packageName ps -> case ps of
285+ p NE.:| [] -> pure p -- No ambiguity here
286+ p NE.:| _ -> do
287+ putLog Warning $ T.pack $
288+ "Packages named '" <> T.unpack packageName <> "' appear in " <> show (length ps) <> " different locations: "
289+ <> intercalate ", " (map _cabalPackageInfo_packageFile $ toList ps)
290+ <> "; Picking " <> _cabalPackageInfo_packageFile p
291+ pure p
292+
293+ packageInfos <- case NE.nonEmpty $ toList unambiguous of
294+ Nothing -> failWith $ T.pack $ "No valid, buildable packages found in " <> intercalate ", " dirs
295+ Just xs -> pure xs
296+
297+ unless (null pkgDirErrs) $
298+ putLog Warning $ T.pack $ "Failed to find buildable packages in " <> intercalate ", " pkgDirErrs
299+
300+ pure packageInfos
301+
302+packageInfoToNamePathMap :: [CabalPackageInfo] -> Map Text FilePath
303+packageInfoToNamePathMap = Map.fromList . map (_cabalPackageInfo_packageName &&& _cabalPackageInfo_packageRoot)
134
304
135-- | Create ghci configuration to load the given packages
305-- | Create ghci configuration to load the given packages
136withGhciScript
306withGhciScript
137 :: MonadObelisk m
307 :: MonadObelisk m
138− => [FilePath] -- ^ List of packages to load into ghci
308+ => [CabalPackageInfo] -- ^ List of packages to load into ghci
139− -> (FilePath -> m ()) -- ^ Action to run with the path to generated temporory .ghci
309+ -> FilePath -- ^ All paths written to the .ghci file will be relative to this path
310+ -> (FilePath -> m ()) -- ^ Action to run with the path to generated temporary .ghci
140 -> m ()
311 -> m ()
141−withGhciScript pkgs f = do
312+withGhciScript packageInfos pathBase f = do
142− (pkgDirErrs, packageInfos) <- fmap partitionEithers $ forM pkgs $ \pkg -> do
313+ selfExe <- liftIO getExecutablePath
143− flip fmap (parseCabalPackage pkg) $ \case
314+ let
144− Nothing -> Left pkg
315+ packageNames = Set.fromList $ map _cabalPackageInfo_packageName packageInfos
145− Just packageInfo -> Right packageInfo
316+ modulesToLoad = mconcat
146−
317+ [ [ "Obelisk.Run" | "obelisk-run" `Set.member` packageNames ]
147− when (null packageInfos) $
318+ , [ "Backend" | "backend" `Set.member` packageNames ]
148− failWith $ T.pack $ "No valid pkgs found in " <> intercalate ", " pkgs
319+ , [ "Frontend" | "frontend" `Set.member` packageNames ]
149− unless (null pkgDirErrs) $
320+ ]
150− putLog Warning $ T.pack $ "Failed to find pkgs in " <> intercalate ", " pkgDirErrs
321+ dotGhci = unlines
151−
322+ -- TODO: Shell escape
152− let extensions = packageInfos >>= _cabalPackageInfo_defaultExtensions
323+ [ ":set -F -pgmF " <> selfExe <> " -optF " <> preprocessorIdentifier <> " " <> unwords (map (("-optF " <>) . makeRelative pathBase . _cabalPackageInfo_packageFile) packageInfos)
153− extensionsLine = if extensions == mempty
324+ , ":set -i" <> intercalate ":" (packageInfos >>= rootedSourceDirs)
154− then ""
325+ , if null modulesToLoad then "" else ":load " <> unwords modulesToLoad
155− else ":set " <> intercalate " " ((("-X" <>) . prettyShow) <$> extensions)
326+ , "import qualified Obelisk.Run"
156− dotGhci = unlines $
327+ , "import qualified Frontend"
157− [ ":set -i" <> intercalate ":" (packageInfos >>= rootedSourceDirs)
328+ , "import qualified Backend" ]
158− , extensionsLine
159− , ":load Backend Frontend"
160− , "import Obelisk.Run"
161− , "import qualified Frontend"
162− , "import qualified Backend"
163− ]
164 withSystemTempDirectory "ob-ghci" $ \fp -> do
329 withSystemTempDirectory "ob-ghci" $ \fp -> do
165 let dotGhciPath = fp </> ".ghci"
330 let dotGhciPath = fp </> ".ghci"
166 liftIO $ writeFile dotGhciPath dotGhci
331 liftIO $ writeFile dotGhciPath dotGhci
@@ -168,31 +333,52 @@ withGhciScript pkgs f = do
168
333
169 where
334 where
170 rootedSourceDirs pkg = NE.toList $
335 rootedSourceDirs pkg = NE.toList $
171− (_cabalPackageInfo_packageRoot pkg </>) <$> _cabalPackageInfo_sourceDirs pkg
336+ makeRelative pathBase . (_cabalPackageInfo_packageRoot pkg </>) <$> _cabalPackageInfo_sourceDirs pkg
172
337
173-- | Run ghci repl
338-- | Run ghci repl
174runGhciRepl
339runGhciRepl
175 :: MonadObelisk m
340 :: MonadObelisk m
176− => FilePath -- ^ Path to .ghci
341+ => FilePath -- ^ Path to project root
342+ -> [CabalPackageInfo]
343+ -> FilePath -- ^ Path to .ghci
177 -> m ()
344 -> m ()
178−runGhciRepl dotGhci = inProjectShell "ghc" $ unwords $ "ghci" : ["-no-user-package-db", "-ghci-script", dotGhci]
345+runGhciRepl root packages dotGhci =
346+ -- NOTE: We do *not* want to use $(staticWhich "ghci") here because we need the
347+ -- ghc that is provided by the shell in the user's project.
348+ nixShellWithPkgs root True False (packageInfoToNamePathMap packages) $ Just $ "ghci " <> makeBaseGhciOptions dotGhci -- TODO: Shell escape
179
349
180-- | Run ghcid
350-- | Run ghcid
181runGhcid
351runGhcid
182 :: MonadObelisk m
352 :: MonadObelisk m
183− => FilePath -- ^ Path to .ghci
353+ => FilePath -- ^ Path to project root
354+ -> Bool -- ^ Should we chdir to root when running this process?
355+ -> FilePath -- ^ Path to .ghci
356+ -> [CabalPackageInfo]
184 -> Maybe String -- ^ Optional command to run at every reload
357 -> Maybe String -- ^ Optional command to run at every reload
185 -> m ()
358 -> m ()
186−runGhcid dotGhci mcmd = callCommand $ unwords $ "ghcid" : opts
359+runGhcid root chdirToRoot dotGhci packages mcmd =
360+ nixShellWithPkgs root True chdirToRoot (packageInfoToNamePathMap packages) (Just $ unwords $ ghcidExePath : opts) -- TODO: Shell escape
187 where
361 where
188 opts =
362 opts =
189 [ "-W"
363 [ "-W"
190− --TODO: The decision of whether to use -fwarn-redundant-constraints should probably be made by the user
364+ , "--command='ghci -ignore-dot-ghci " <> makeBaseGhciOptions dotGhci <> "' "
191− , "--command='ghci -Wall -ignore-dot-ghci -fwarn-redundant-constraints -no-user-package-db -ghci-script " <> dotGhci <> "' "
192− , "--reload=config"
193 , "--outputfile=ghcid-output.txt"
365 , "--outputfile=ghcid-output.txt"
194− ] <> testCmd
366+ ] <> map (\x -> "--reload='" <> x <> "'") reloadFiles
195− testCmd = maybeToList (flip fmap mcmd $ \cmd -> "--test='" <> cmd <> "'")
367+ <> map (\x -> "--restart='" <> x <> "'") restartFiles
368+ <> testCmd
369+ testCmd = maybeToList (flip fmap mcmd $ \cmd -> "--test='" <> cmd <> "'") -- TODO: Shell escape
370+
371+ adjustRoot x = if chdirToRoot then makeRelative root x else x
372+ reloadFiles = map adjustRoot [root </> "config"]
373+ restartFiles = map (adjustRoot . _cabalPackageInfo_packageFile) packages
374+
375+makeBaseGhciOptions :: FilePath -> String
376+makeBaseGhciOptions dotGhci =
377+ unwords
378+ [ "-no-user-package-db"
379+ , "-package-env -"
380+ , "-ghci-script " <> dotGhci
381+ ]
196
382
197getFreePort :: MonadIO m => m PortNumber
383getFreePort :: MonadIO m => m PortNumber
198getFreePort = liftIO $ withSocketsDo $ do
384getFreePort = liftIO $ withSocketsDo $ do
lib/command/src/Obelisk/Command/Thunk.hs+259−127
@@ -8,26 +8,31 @@
8{-# LANGUAGE ScopedTypeVariables #-}
8{-# LANGUAGE ScopedTypeVariables #-}
9{-# LANGUAGE TypeApplications #-}
9{-# LANGUAGE TypeApplications #-}
10module Obelisk.Command.Thunk
10module Obelisk.Command.Thunk
11− ( ThunkPtr (..)
11+ ( GitHubSource (..)
12+ , GitUri (..)
13+ , ReadThunkError (..)
14+ , ThunkConfig (..)
15+ , ThunkData (..)
16+ , ThunkPackConfig (..)
17+ , ThunkPtr (..)
12 , ThunkRev (..)
18 , ThunkRev (..)
13 , ThunkSource (..)
19 , ThunkSource (..)
14− , ThunkData (..)
20+ , ThunkUpdateConfig (..)
15− , ReadThunkError (..)
21+ , attrCacheFileName
16− , GitHubSource (..)
17− , getThunkGitBranch
18− , getLatestRev
19− , updateThunkToLatest
20 , createThunk
22 , createThunk
21 , createThunkWithLatest
23 , createThunkWithLatest
24+ , getLatestRev
25+ , getThunkGitBranch
26+ , getThunkPtr
27+ , gitUriToText
22 , nixBuildAttrWithCache
28 , nixBuildAttrWithCache
23 , nixBuildThunkAttrWithCache
29 , nixBuildThunkAttrWithCache
24− , unpackThunk
25 , packThunk
30 , packThunk
31+ , parseGitUri
26 , readThunk
32 , readThunk
33+ , unpackThunk
27 , updateThunk
34 , updateThunk
28− , getThunkPtr
35+ , updateThunkToLatest
29− , getThunkPtr'
30− , parseGitUri
31 , uriThunkPtr
36 , uriThunkPtr
32 ) where
37 ) where
33
38
@@ -43,6 +48,7 @@ import qualified Data.Aeson as Aeson
43import Data.Aeson.Encode.Pretty
48import Data.Aeson.Encode.Pretty
44import qualified Data.Aeson.Types as Aeson
49import qualified Data.Aeson.Types as Aeson
45import qualified Data.ByteString.Lazy as LBS
50import qualified Data.ByteString.Lazy as LBS
51+import Data.Containers.ListUtils (nubOrd)
46import Data.Default
52import Data.Default
47import Data.Either.Combinators (fromRight', rightToMaybe)
53import Data.Either.Combinators (fromRight', rightToMaybe)
48import Data.Git.Ref (Ref)
54import Data.Git.Ref (Ref)
@@ -65,12 +71,12 @@ import GitHub
65import GitHub.Data.Name
71import GitHub.Data.Name
66import Obelisk.Command.Nix
72import Obelisk.Command.Nix
67import System.Directory
73import System.Directory
74+import System.Exit
68import System.FilePath
75import System.FilePath
69import System.IO.Error
76import System.IO.Error
70import System.IO.Temp
77import System.IO.Temp
71import System.Posix (getSymbolicLinkStatus, modificationTime)
78import System.Posix (getSymbolicLinkStatus, modificationTime)
72−import System.Process (proc)
79+import qualified Text.URI as URI
73−import Text.URI
74
80
75import Obelisk.App (MonadObelisk)
81import Obelisk.App (MonadObelisk)
76import Obelisk.CliApp
82import Obelisk.CliApp
@@ -95,7 +101,7 @@ type NixSha256 = Text --TODO: Use a smart constructor and make this actually ver
95
101
96-- | A specific revision of data; it may be available from multiple sources
102-- | A specific revision of data; it may be available from multiple sources
97data ThunkRev = ThunkRev
103data ThunkRev = ThunkRev
98− { _thunkRev_commit :: Ref
104+ { _thunkRev_commit :: Ref Ref.SHA1
99 , _thunkRev_nixSha256 :: NixSha256
105 , _thunkRev_nixSha256 :: NixSha256
100 }
106 }
101 deriving (Show, Eq, Ord)
107 deriving (Show, Eq, Ord)
@@ -112,37 +118,63 @@ data GitHubSource = GitHubSource
112 { _gitHubSource_owner :: Name Owner
118 { _gitHubSource_owner :: Name Owner
113 , _gitHubSource_repo :: Name Repo
119 , _gitHubSource_repo :: Name Repo
114 , _gitHubSource_branch :: Maybe (Name Branch)
120 , _gitHubSource_branch :: Maybe (Name Branch)
121+ , _gitHubSource_private :: Bool
115 }
122 }
116 deriving (Show, Eq, Ord)
123 deriving (Show, Eq, Ord)
117
124
125+newtype GitUri = GitUri { unGitUri :: URI.URI } deriving (Eq, Ord, Show)
126+
127+gitUriToText :: GitUri -> Text
128+gitUriToText (GitUri uri)
129+ | (T.toLower . URI.unRText <$> URI.uriScheme uri) == Just "file"
130+ , Just (_, path) <- URI.uriPath uri
131+ = "/" <> T.intercalate "/" (map URI.unRText $ NonEmpty.toList path)
132+ | otherwise = URI.render uri
133+
118data GitSource = GitSource
134data GitSource = GitSource
119− { _gitSource_url :: URI
135+ { _gitSource_url :: GitUri
120 , _gitSource_branch :: Maybe (Name Branch)
136 , _gitSource_branch :: Maybe (Name Branch)
121 , _gitSource_fetchSubmodules :: Bool
137 , _gitSource_fetchSubmodules :: Bool
138+ , _gitSource_private :: Bool
122 }
139 }
123 deriving (Show, Eq, Ord)
140 deriving (Show, Eq, Ord)
124
141
142+newtype ThunkConfig = ThunkConfig
143+ { _thunkConfig_private :: Maybe Bool
144+ } deriving Show
145+
146+data ThunkUpdateConfig = ThunkUpdateConfig
147+ { _thunkUpdateConfig_branch :: Maybe String
148+ , _thunkUpdateConfig_config :: ThunkConfig
149+ } deriving Show
150+
151+data ThunkPackConfig = ThunkPackConfig
152+ { _thunkPackConfig_force :: Bool
153+ , _thunkPackConfig_config :: ThunkConfig
154+ } deriving Show
155+
125-- | Convert a GitHub source to a regular Git source. Assumes no submodules.
156-- | Convert a GitHub source to a regular Git source. Assumes no submodules.
126forgetGithub :: Bool -> GitHubSource -> GitSource
157forgetGithub :: Bool -> GitHubSource -> GitSource
127forgetGithub useSsh s = GitSource
158forgetGithub useSsh s = GitSource
128− { _gitSource_url = URI
159+ { _gitSource_url = GitUri $ URI.URI
129− { uriScheme = Just $ fromRight' $ mkScheme $ if useSsh then "ssh" else "https"
160+ { URI.uriScheme = Just $ fromRight' $ URI.mkScheme $ if useSsh then "ssh" else "https"
130− , uriAuthority = Right $ Authority
161+ , URI.uriAuthority = Right $ URI.Authority
131− { authUserInfo = UserInfo (fromRight' $ mkUsername "git") Nothing
162+ { URI.authUserInfo = URI.UserInfo (fromRight' $ URI.mkUsername "git") Nothing
132 <$ guard useSsh
163 <$ guard useSsh
133− , authHost = fromRight' $ mkHost "github.com"
164+ , URI.authHost = fromRight' $ URI.mkHost "github.com"
134− , authPort = Nothing
165+ , URI.authPort = Nothing
135 }
166 }
136− , uriPath = Just ( False
167+ , URI.uriPath = Just ( False
137− , fromRight' . mkPathPiece <$>
168+ , fromRight' . URI.mkPathPiece <$>
138 untagName (_gitHubSource_owner s)
169 untagName (_gitHubSource_owner s)
139 :| [ untagName (_gitHubSource_repo s) <> ".git" ]
170 :| [ untagName (_gitHubSource_repo s) <> ".git" ]
140 )
171 )
141− , uriQuery = []
172+ , URI.uriQuery = []
142− , uriFragment = Nothing
173+ , URI.uriFragment = Nothing
143 }
174 }
144 , _gitSource_branch = _gitHubSource_branch s
175 , _gitSource_branch = _gitHubSource_branch s
145 , _gitSource_fetchSubmodules = False
176 , _gitSource_fetchSubmodules = False
177+ , _gitSource_private = _gitHubSource_private s
146 }
178 }
147
179
148getThunkGitBranch :: ThunkPtr -> Maybe Text
180getThunkGitBranch :: ThunkPtr -> Maybe Text
@@ -150,26 +182,26 @@ getThunkGitBranch (ThunkPtr _ src) = fmap untagName $ case src of
150 ThunkSource_GitHub s -> _gitHubSource_branch s
182 ThunkSource_GitHub s -> _gitHubSource_branch s
151 ThunkSource_Git s -> _gitSource_branch s
183 ThunkSource_Git s -> _gitSource_branch s
152
184
153−commitNameToRef :: Name Commit -> Ref
185+commitNameToRef :: Name Commit -> Ref Ref.SHA1
154commitNameToRef (N c) = Ref.fromHex $ encodeUtf8 c
186commitNameToRef (N c) = Ref.fromHex $ encodeUtf8 c
155
187
156-- TODO: Use spinner here.
188-- TODO: Use spinner here.
157getNixSha256ForUriUnpacked
189getNixSha256ForUriUnpacked
158 :: MonadObelisk m
190 :: MonadObelisk m
159− => URI
191+ => GitUri
160 -> m NixSha256
192 -> m NixSha256
161getNixSha256ForUriUnpacked uri =
193getNixSha256ForUriUnpacked uri =
162− withExitFailMessage ("nix-prefetch-url: Failed to determine sha256 hash of URL " <> render uri) $ do
194+ withExitFailMessage ("nix-prefetch-url: Failed to determine sha256 hash of URL " <> gitUriToText uri) $ do
163 [hash] <- fmap T.lines $ readProcessAndLogOutput (Debug, Debug) $
195 [hash] <- fmap T.lines $ readProcessAndLogOutput (Debug, Debug) $
164− proc "nix-prefetch-url" ["--unpack" , "--type" , "sha256" , T.unpack $ render uri]
196+ proc "nix-prefetch-url" ["--unpack", "--type", "sha256", T.unpack $ gitUriToText uri]
165 pure hash
197 pure hash
166
198
167−nixPrefetchGit :: MonadObelisk m => URI -> Text -> Bool -> m NixSha256
199+nixPrefetchGit :: MonadObelisk m => GitUri -> Text -> Bool -> m NixSha256
168nixPrefetchGit uri rev fetchSubmodules =
200nixPrefetchGit uri rev fetchSubmodules =
169− withExitFailMessage ("nix-prefetch-git: Failed to determine sha256 hash of Git repo " <> render uri <> " at " <> rev) $ do
201+ withExitFailMessage ("nix-prefetch-git: Failed to determine sha256 hash of Git repo " <> gitUriToText uri <> " at " <> rev) $ do
170 out <- readProcessAndLogStderr Debug $
202 out <- readProcessAndLogStderr Debug $
171 proc "nix-prefetch-git" $ filter (/="")
203 proc "nix-prefetch-git" $ filter (/="")
172− [ "--url", T.unpack $ render uri
204+ [ "--url", T.unpack $ gitUriToText uri
173 , "--rev", T.unpack rev
205 , "--rev", T.unpack rev
174 , if fetchSubmodules then "--fetch-submodules" else ""
206 , if fetchSubmodules then "--fetch-submodules" else ""
175 , "--quiet"
207 , "--quiet"
@@ -200,6 +232,9 @@ readThunk thunkDir = do
200 False -> return $ return Nothing
232 False -> return $ return Nothing
201 False -> liftIO $ fmap ThunkData_Packed <$> readPackedThunk thunkDir
233 False -> liftIO $ fmap ThunkData_Packed <$> readPackedThunk thunkDir
202
234
235+attrCacheFileName :: FilePath
236+attrCacheFileName = ".attr-cache"
237+
203data ThunkType = ThunkType
238data ThunkType = ThunkType
204 { _thunkType_loader :: FilePath
239 { _thunkType_loader :: FilePath
205 , _thunkType_json :: FilePath
240 , _thunkType_json :: FilePath
@@ -212,7 +247,7 @@ gitHubThunkType :: ThunkType
212gitHubThunkType = ThunkType
247gitHubThunkType = ThunkType
213 { _thunkType_loader = "default.nix"
248 { _thunkType_loader = "default.nix"
214 , _thunkType_json = "github.json"
249 , _thunkType_json = "github.json"
215− , _thunkType_optional = Set.fromList [".attr-cache"]
250+ , _thunkType_optional = Set.fromList [attrCacheFileName]
216 , _thunkType_loaderVersions = gitHubStandaloneLoaders
251 , _thunkType_loaderVersions = gitHubStandaloneLoaders
217 , _thunkType_parser = parseThunkPtr $ \v ->
252 , _thunkType_parser = parseThunkPtr $ \v ->
218 ThunkSource_GitHub <$> parseGitHubSource v <|> ThunkSource_Git <$> parseGitSource v
253 ThunkSource_GitHub <$> parseGitHubSource v <|> ThunkSource_Git <$> parseGitSource v
@@ -222,7 +257,7 @@ gitThunkType :: ThunkType
222gitThunkType = ThunkType
257gitThunkType = ThunkType
223 { _thunkType_loader = "default.nix"
258 { _thunkType_loader = "default.nix"
224 , _thunkType_json = "git.json"
259 , _thunkType_json = "git.json"
225− , _thunkType_optional = Set.fromList [".attr-cache"]
260+ , _thunkType_optional = Set.fromList [attrCacheFileName]
226 , _thunkType_loaderVersions = plainGitStandaloneLoaders
261 , _thunkType_loaderVersions = plainGitStandaloneLoaders
227 , _thunkType_parser = parseThunkPtr $ fmap ThunkSource_Git . parseGitSource
262 , _thunkType_parser = parseThunkPtr $ fmap ThunkSource_Git . parseGitSource
228 }
263 }
@@ -287,10 +322,12 @@ parseGitHubSource v = do
287 owner <- v Aeson..: "owner"
322 owner <- v Aeson..: "owner"
288 repo <- v Aeson..: "repo"
323 repo <- v Aeson..: "repo"
289 branch <- v Aeson..:! "branch"
324 branch <- v Aeson..:! "branch"
325+ private <- v Aeson..:? "private"
290 pure $ GitHubSource
326 pure $ GitHubSource
291 { _gitHubSource_owner = owner
327 { _gitHubSource_owner = owner
292 , _gitHubSource_repo = repo
328 , _gitHubSource_repo = repo
293 , _gitHubSource_branch = branch
329 , _gitHubSource_branch = branch
330+ , _gitHubSource_private = fromMaybe False private
294 }
331 }
295
332
296parseGitSource :: Aeson.Object -> Aeson.Parser GitSource
333parseGitSource :: Aeson.Object -> Aeson.Parser GitSource
@@ -298,10 +335,12 @@ parseGitSource v = do
298 Just url <- parseGitUri <$> v Aeson..: "url"
335 Just url <- parseGitUri <$> v Aeson..: "url"
299 branch <- v Aeson..:! "branch"
336 branch <- v Aeson..:! "branch"
300 fetchSubmodules <- v Aeson..:! "fetchSubmodules"
337 fetchSubmodules <- v Aeson..:! "fetchSubmodules"
338+ private <- v Aeson..:? "private"
301 pure $ GitSource
339 pure $ GitSource
302 { _gitSource_url = url
340 { _gitSource_url = url
303 , _gitSource_branch = branch
341 , _gitSource_branch = branch
304 , _gitSource_fetchSubmodules = fromMaybe False fetchSubmodules
342 , _gitSource_fetchSubmodules = fromMaybe False fetchSubmodules
343+ , _gitSource_private = fromMaybe False private
305 }
344 }
306
345
307overwriteThunk :: MonadObelisk m => FilePath -> ThunkPtr -> m ()
346overwriteThunk :: MonadObelisk m => FilePath -> ThunkPtr -> m ()
@@ -328,13 +367,15 @@ encodeThunkPtrData (ThunkPtr rev src) = case src of
328 , ("branch" .=) <$> _gitHubSource_branch s
367 , ("branch" .=) <$> _gitHubSource_branch s
329 , Just $ "rev" .= Ref.toHexString (_thunkRev_commit rev)
368 , Just $ "rev" .= Ref.toHexString (_thunkRev_commit rev)
330 , Just $ "sha256" .= _thunkRev_nixSha256 rev
369 , Just $ "sha256" .= _thunkRev_nixSha256 rev
370+ , Just $ "private" .= _gitHubSource_private s
331 ]
371 ]
332 ThunkSource_Git s -> encodePretty' plainGitCfg $ Aeson.object $ catMaybes
372 ThunkSource_Git s -> encodePretty' plainGitCfg $ Aeson.object $ catMaybes
333− [ Just $ "url" .= render (_gitSource_url s)
373+ [ Just $ "url" .= gitUriToText (_gitSource_url s)
334 , Just $ "rev" .= Ref.toHexString (_thunkRev_commit rev)
374 , Just $ "rev" .= Ref.toHexString (_thunkRev_commit rev)
335 , ("branch" .=) <$> _gitSource_branch s
375 , ("branch" .=) <$> _gitSource_branch s
336 , Just $ "sha256" .= _thunkRev_nixSha256 rev
376 , Just $ "sha256" .= _thunkRev_nixSha256 rev
337 , Just $ "fetchSubmodules" .= _gitSource_fetchSubmodules s
377 , Just $ "fetchSubmodules" .= _gitSource_fetchSubmodules s
378+ , Just $ "private" .= _gitSource_private s
338 ]
379 ]
339 where
380 where
340 githubCfg = defConfig
381 githubCfg = defConfig
@@ -355,6 +396,7 @@ encodeThunkPtrData (ThunkPtr rev src) = case src of
355 [ "url"
396 [ "url"
356 , "rev"
397 , "rev"
357 , "sha256"
398 , "sha256"
399+ , "private"
358 , "fetchSubmodules"
400 , "fetchSubmodules"
359 ] <> compare
401 ] <> compare
360 , confTrailingNewline = True
402 , confTrailingNewline = True
@@ -362,7 +404,7 @@ encodeThunkPtrData (ThunkPtr rev src) = case src of
362
404
363createThunk :: MonadIO m => FilePath -> ThunkPtr -> m ()
405createThunk :: MonadIO m => FilePath -> ThunkPtr -> m ()
364createThunk target thunk = liftIO $ do
406createThunk target thunk = liftIO $ do
365− createDirectoryIfMissing True (target </> ".attr-cache")
407+ createDirectoryIfMissing True (target </> attrCacheFileName)
366 T.writeFile (target </> "default.nix") (thunkPtrLoader thunk)
408 T.writeFile (target </> "default.nix") (thunkPtrLoader thunk)
367 let
409 let
368 jsonFileName = case _thunkPtr_source thunk of
410 jsonFileName = case _thunkPtr_source thunk of
@@ -378,8 +420,8 @@ createThunkWithLatest target s = do
378 , _thunkPtr_rev = rev
420 , _thunkPtr_rev = rev
379 }
421 }
380
422
381−updateThunkToLatest :: MonadObelisk m => FilePath -> Maybe String -> m ()
423+updateThunkToLatest :: MonadObelisk m => ThunkUpdateConfig -> FilePath -> m ()
382−updateThunkToLatest target mBranch = withSpinner' ("Updating thunk " <> T.pack target <> " to latest") (pure $ const $ "Thunk " <> T.pack target <> " updated to latest") $ do
424+updateThunkToLatest (ThunkUpdateConfig mBranch thunkConfig) target = withSpinner' ("Updating thunk " <> T.pack target <> " to latest") (pure $ const $ "Thunk " <> T.pack target <> " updated to latest") $ do
383 checkThunkDirectory "ob thunk update directory cannot be '.'" target
425 checkThunkDirectory "ob thunk update directory cannot be '.'" target
384 -- check to see if thunk should be updated to a specific branch or just update it's current branch
426 -- check to see if thunk should be updated to a specific branch or just update it's current branch
385 case mBranch of
427 case mBranch of
@@ -391,7 +433,7 @@ updateThunkToLatest target mBranch = withSpinner' ("Updating thunk " <> T.pack t
391 ThunkData_Checkout _ -> failWith "cannot update an unpacked thunk"
433 ThunkData_Checkout _ -> failWith "cannot update an unpacked thunk"
392 let src = _thunkPtr_source ptr
434 let src = _thunkPtr_source ptr
393 rev <- getLatestRev src
435 rev <- getLatestRev src
394− overwriteThunk overwrite $ ThunkPtr
436+ overwriteThunk overwrite $ modifyThunkPtrByConfig thunkConfig $ ThunkPtr
395 { _thunkPtr_source = src
437 { _thunkPtr_source = src
396 , _thunkPtr_rev = rev
438 , _thunkPtr_rev = rev
397 }
439 }
@@ -399,24 +441,25 @@ updateThunkToLatest target mBranch = withSpinner' ("Updating thunk " <> T.pack t
399 Left err -> failWith $ T.pack $ "thunk update: " <> show err
441 Left err -> failWith $ T.pack $ "thunk update: " <> show err
400 Right c -> case c of
442 Right c -> case c of
401 ThunkData_Packed t -> case _thunkPtr_source t of
443 ThunkData_Packed t -> case _thunkPtr_source t of
402− ThunkSource_Git tsg -> setThunk target tsg branch
444+ ThunkSource_Git tsg -> setThunk thunkConfig target tsg branch
403 ThunkSource_GitHub tsgh -> do
445 ThunkSource_GitHub tsgh -> do
404 let tsg = forgetGithub False tsgh
446 let tsg = forgetGithub False tsgh
405− setThunk target tsg branch
447+ setThunk thunkConfig target tsg branch
406− 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."
448+ ThunkData_Checkout _ -> failWith $ T.pack $ "thunk located at " <> show target <> " is unpacked. Use ob thunk pack on the desired directory and then try ob thunk update again."
407
449
408−setThunk :: MonadObelisk m => FilePath -> GitSource -> String -> m ()
450+setThunk :: MonadObelisk m => ThunkConfig -> FilePath -> GitSource -> String -> m ()
409−setThunk target gs branch = do
451+setThunk thunkConfig target gs branch = do
410− newThunkPtr <- uriThunkPtr (_gitSource_url gs) (Just $ T.pack branch) Nothing
452+ newThunkPtr <- uriThunkPtr (_gitSource_url gs) (_thunkConfig_private thunkConfig) (Just $ T.pack branch) Nothing
411 overwriteThunk target newThunkPtr
453 overwriteThunk target newThunkPtr
412− updateThunkToLatest target Nothing
454+ updateThunkToLatest (ThunkUpdateConfig Nothing thunkConfig) target
413
455
414-- | All recognized github standalone loaders, ordered from newest to oldest.
456-- | All recognized github standalone loaders, ordered from newest to oldest.
415-- This tool will only ever produce the newest one when it writes a thunk.
457-- This tool will only ever produce the newest one when it writes a thunk.
416gitHubStandaloneLoaders :: NonEmpty Text
458gitHubStandaloneLoaders :: NonEmpty Text
417gitHubStandaloneLoaders =
459gitHubStandaloneLoaders =
418− gitHubStandaloneLoaderV2 :|
460+ gitHubStandaloneLoaderV4 :|
419 [ gitHubStandaloneLoaderV3
461 [ gitHubStandaloneLoaderV3
462+ , gitHubStandaloneLoaderV2
420 , gitHubStandaloneLoaderV1
463 , gitHubStandaloneLoaderV1
421 ]
464 ]
422
465
@@ -456,10 +499,24 @@ gitHubStandaloneLoaderV3 = T.unlines
456 , "in import (fetch (builtins.fromJSON (builtins.readFile ./github.json)))"
499 , "in import (fetch (builtins.fromJSON (builtins.readFile ./github.json)))"
457 ]
500 ]
458
501
502+gitHubStandaloneLoaderV4 :: Text
503+gitHubStandaloneLoaderV4 = T.unlines
504+ [ "# DO NOT HAND-EDIT THIS FILE"
505+ , "let fetch = { private ? false, fetchSubmodules ? false, owner, repo, rev, sha256, ... }:"
506+ , " if !fetchSubmodules && !private then builtins.fetchTarball {"
507+ , " url = \"https://github.com/${owner}/${repo}/archive/${rev}.tar.gz\"; inherit sha256;"
508+ , " } else (import <nixpkgs> {}).fetchFromGitHub {"
509+ , " inherit owner repo rev sha256 fetchSubmodules private;"
510+ , " };"
511+ , "in import (fetch (builtins.fromJSON (builtins.readFile ./github.json)))"
512+ ]
513+
459plainGitStandaloneLoaders :: NonEmpty Text
514plainGitStandaloneLoaders :: NonEmpty Text
460plainGitStandaloneLoaders =
515plainGitStandaloneLoaders =
461− plainGitStandaloneLoaderV1 :|
516+ plainGitStandaloneLoaderV4 :|
462− [ plainGitStandaloneLoaderV2
517+ [ plainGitStandaloneLoaderV1
518+ , plainGitStandaloneLoaderV2
519+ , plainGitStandaloneLoaderV3
463 ]
520 ]
464
521
465plainGitStandaloneLoaderV1 :: Text
522plainGitStandaloneLoaderV1 :: Text
@@ -480,6 +537,41 @@ plainGitStandaloneLoaderV2 = T.unlines
480 , "in import (fetchGit (builtins.fromJSON (builtins.readFile ./git.json)))"
537 , "in import (fetchGit (builtins.fromJSON (builtins.readFile ./git.json)))"
481 ]
538 ]
482
539
540+-- This loader has a bug because @builtins.fetchGit@ is not given a @ref@
541+-- and will fail to find commits without this because it does shallow clones.
542+plainGitStandaloneLoaderV3 :: Text
543+plainGitStandaloneLoaderV3 = T.unlines
544+ [ "# DO NOT HAND-EDIT THIS FILE"
545+ , "let fetch = {url, rev, ref ? null, sha256 ? null, fetchSubmodules ? false, private ? false, ...}:"
546+ , " let realUrl = let firstChar = builtins.substring 0 1 url; in"
547+ , " if firstChar == \"/\" then /. + url"
548+ , " else if firstChar == \".\" then ./. + url"
549+ , " else url;"
550+ , " in if !fetchSubmodules && private then builtins.fetchGit {"
551+ , " url = realUrl; inherit rev;"
552+ , " } else (import <nixpkgs> {}).fetchgit {"
553+ , " url = realUrl; inherit rev sha256;"
554+ , " };"
555+ , "in import (fetch (builtins.fromJSON (builtins.readFile ./git.json)))"
556+ ]
557+
558+plainGitStandaloneLoaderV4 :: Text
559+plainGitStandaloneLoaderV4 = T.unlines
560+ [ "# DO NOT HAND-EDIT THIS FILE"
561+ , "let fetch = {url, rev, branch ? null, sha256 ? null, fetchSubmodules ? false, private ? false, ...}:"
562+ , " let realUrl = let firstChar = builtins.substring 0 1 url; in"
563+ , " if firstChar == \"/\" then /. + url"
564+ , " else if firstChar == \".\" then ./. + url"
565+ , " else url;"
566+ , " in if !fetchSubmodules && private then builtins.fetchGit {"
567+ , " url = realUrl; inherit rev;"
568+ , " ${if branch == null then null else \"ref\"} = branch;"
569+ , " } else (import <nixpkgs> {}).fetchgit {"
570+ , " url = realUrl; inherit rev sha256;"
571+ , " };"
572+ , "in import (fetch (builtins.fromJSON (builtins.readFile ./git.json)))"
573+ ]
574+
483thunkFileNames :: [FilePath]
575thunkFileNames :: [FilePath]
484thunkFileNames = ["default.nix", "github.json", "git.json"]
576thunkFileNames = ["default.nix", "github.json", "git.json"]
485
577
@@ -506,7 +598,7 @@ nixBuildThunkAttrWithCache thunkDir attr = do
506 let cacheErrHandler e
598 let cacheErrHandler e
507 | isDoesNotExistError e = return Nothing -- expected from a cache miss
599 | isDoesNotExistError e = return Nothing -- expected from a cache miss
508 | otherwise = putLog Error (T.pack $ displayException e) >> return Nothing
600 | otherwise = putLog Error (T.pack $ displayException e) >> return Nothing
509− cacheDir = thunkDir </> ".attr-cache"
601+ cacheDir = thunkDir </> attrCacheFileName
510 cachePath = cacheDir </> attr <.> "out"
602 cachePath = cacheDir </> attr <.> "out"
511 latestChange <- liftIO $ do
603 latestChange <- liftIO $ do
512 createDirectoryIfMissing False cacheDir
604 createDirectoryIfMissing False cacheDir
@@ -521,7 +613,7 @@ nixBuildThunkAttrWithCache thunkDir attr = do
521 Just c -> return c
613 Just c -> return c
522 Nothing -> do
614 Nothing -> do
523 putLog Warning $ T.pack $ mconcat [thunkDir, ": ", attr, " not cached, building ..."]
615 putLog Warning $ T.pack $ mconcat [thunkDir, ": ", attr, " not cached, building ..."]
524− _ <- nixCmd $ NixCmd_Build$ def
616+ _ <- nixCmd $ NixCmd_Build $ def
525 Lens.& nixBuildConfig_outLink Lens..~ OutLink_IndirectRoot cachePath
617 Lens.& nixBuildConfig_outLink Lens..~ OutLink_IndirectRoot cachePath
526 Lens.& nixCmdConfig_target Lens..~ Target
618 Lens.& nixCmdConfig_target Lens..~ Target
527 { _target_path = Just thunkDir
619 { _target_path = Just thunkDir
@@ -569,13 +661,13 @@ updateThunk p f = withSystemTempDirectory "obelisk-thunkptr-" $ \tmpDir -> do
569 Right (ThunkData_Packed _) -> do
661 Right (ThunkData_Packed _) -> do
570 let tmpThunk = tmpDir </> "thunk"
662 let tmpThunk = tmpDir </> "thunk"
571 callProcessAndLogOutput (Notice, Error) $
663 callProcessAndLogOutput (Notice, Error) $
572− proc "cp" ["-r", "-T", thunkDir, tmpThunk]
664+ proc cp ["-r", "-T", thunkDir, tmpThunk]
573 return tmpThunk
665 return tmpThunk
574− Right _ -> failWith $ "Thunk is not packed"
666+ Right _ -> failWith "Thunk is not packed"
575 updateThunkFromTmp p' = do
667 updateThunkFromTmp p' = do
576− _ <- packThunk' True p'
668+ _ <- packThunk' True (ThunkPackConfig False (ThunkConfig Nothing)) p'
577 callProcessAndLogOutput (Notice, Error) $
669 callProcessAndLogOutput (Notice, Error) $
578− proc "cp" ["-r", "-T", p', p]
670+ proc cp ["-r", "-T", p', p]
579
671
580finalMsg :: Bool -> (a -> Text) -> Maybe (a -> Text)
672finalMsg :: Bool -> (a -> Text) -> Maybe (a -> Text)
581finalMsg noTrail s = if noTrail then Nothing else Just s
673finalMsg noTrail s = if noTrail then Nothing else Just s
@@ -607,7 +699,7 @@ unpackThunk' noTrail thunkDir = checkThunkDirectory "Can't pack/unpack from with
607 let git = callProcessAndLogOutput (Notice, Notice) . gitProc tmpRepo
699 let git = callProcessAndLogOutput (Notice, Notice) . gitProc tmpRepo
608 git $ [ "clone" ]
700 git $ [ "clone" ]
609 ++ ("--recursive" <$ guard (_gitSource_fetchSubmodules s))
701 ++ ("--recursive" <$ guard (_gitSource_fetchSubmodules s))
610− ++ [ T.unpack $ render $ _gitSource_url s ]
702+ ++ [ T.unpack $ gitUriToText $ _gitSource_url s ]
611 ++ do branch <- maybeToList $ _gitSource_branch s
703 ++ do branch <- maybeToList $ _gitSource_branch s
612 [ "--branch", T.unpack $ untagName branch ]
704 [ "--branch", T.unpack $ untagName branch ]
613 git ["reset", "--hard", Ref.toHexString $ _thunkRev_commit $ _thunkPtr_rev tptr]
705 git ["reset", "--hard", Ref.toHexString $ _thunkRev_commit $ _thunkPtr_rev tptr]
@@ -616,35 +708,39 @@ unpackThunk' noTrail thunkDir = checkThunkDirectory "Can't pack/unpack from with
616
708
617 liftIO $ createDirectory obGitDir
709 liftIO $ createDirectory obGitDir
618 callProcessAndLogOutput (Notice, Error) $
710 callProcessAndLogOutput (Notice, Error) $
619− proc "cp" ["-r", "-T", thunkDir </> ".", obGitDir </> "orig-thunk"]
711+ proc cp ["-r", "-T", thunkDir </> ".", obGitDir </> "orig-thunk"]
620 callProcessAndLogOutput (Notice, Error) $
712 callProcessAndLogOutput (Notice, Error) $
621 proc "rm" ["-r", thunkDir]
713 proc "rm" ["-r", thunkDir]
622 callProcessAndLogOutput (Notice, Error) $
714 callProcessAndLogOutput (Notice, Error) $
623 proc "mv" ["-T", tmpRepo, thunkDir]
715 proc "mv" ["-T", tmpRepo, thunkDir]
624
716
625−--TODO: add force mode to pack even if changes are present
626--TODO: add a rollback mode to pack to the original thunk
717--TODO: add a rollback mode to pack to the original thunk
627−packThunk :: MonadObelisk m => FilePath -> m ThunkPtr
718+packThunk :: MonadObelisk m => ThunkPackConfig -> FilePath -> m ThunkPtr
628packThunk = packThunk' False
719packThunk = packThunk' False
629
720
630−packThunk' :: MonadObelisk m => Bool -> FilePath -> m ThunkPtr
721+packThunk' :: MonadObelisk m => Bool -> ThunkPackConfig -> FilePath -> m ThunkPtr
631−packThunk' noTrail thunkDir = checkThunkDirectory "Can't pack/unpack from within the thunk directory" thunkDir >> readThunk thunkDir >>= \case
722+packThunk' noTrail (ThunkPackConfig force thunkConfig) thunkDir = checkThunkDirectory "Can't pack/unpack from within the thunk directory" thunkDir >> readThunk thunkDir >>= \case
632 Left err -> failWith $ T.pack $ "thunk pack: " <> show err
723 Left err -> failWith $ T.pack $ "thunk pack: " <> show err
633 Right (ThunkData_Packed _) -> failWith "pack: thunk is already packed"
724 Right (ThunkData_Packed _) -> failWith "pack: thunk is already packed"
634 Right (ThunkData_Checkout _) -> do
725 Right (ThunkData_Checkout _) -> do
635 withSpinner' ("Packing thunk " <> T.pack thunkDir)
726 withSpinner' ("Packing thunk " <> T.pack thunkDir)
636 (finalMsg noTrail $ const $ "Packed thunk " <> T.pack thunkDir) $ do
727 (finalMsg noTrail $ const $ "Packed thunk " <> T.pack thunkDir) $ do
637− thunkPtr <- getThunkPtr thunkDir
728+ thunkPtr <- modifyThunkPtrByConfig thunkConfig <$> getThunkPtr (not force) thunkDir (_thunkConfig_private thunkConfig)
638 callProcessAndLogOutput (Debug, Error) $ proc "rm" ["-rf", thunkDir]
729 callProcessAndLogOutput (Debug, Error) $ proc "rm" ["-rf", thunkDir]
639 liftIO $ createThunk thunkDir thunkPtr
730 liftIO $ createThunk thunkDir thunkPtr
640 pure thunkPtr
731 pure thunkPtr
641
732
642−getThunkPtr :: MonadObelisk m => FilePath -> m ThunkPtr
733+modifyThunkPtrByConfig :: ThunkConfig -> ThunkPtr -> ThunkPtr
643−getThunkPtr = getThunkPtr' True
734+modifyThunkPtrByConfig (ThunkConfig markPrivate') ptr = case markPrivate' of
735+ Nothing -> ptr
736+ Just markPrivate -> ptr { _thunkPtr_source = case _thunkPtr_source ptr of
737+ ThunkSource_Git s -> ThunkSource_Git $ s { _gitSource_private = markPrivate }
738+ ThunkSource_GitHub s -> ThunkSource_GitHub $ s { _gitHubSource_private = markPrivate }
739+ }
644
740
645−getThunkPtr' :: forall m. MonadObelisk m => Bool -> FilePath -> m ThunkPtr
741+getThunkPtr :: forall m. MonadObelisk m => Bool -> FilePath -> Maybe Bool -> m ThunkPtr
646−getThunkPtr' checkClean thunkDir = do
742+getThunkPtr checkClean thunkDir mPrivate = do
647− when checkClean $ ensureCleanGitRepo thunkDir True $
743+ when checkClean $ ensureCleanGitRepo thunkDir True
648 "thunk pack: thunk checkout contains unsaved modifications"
744 "thunk pack: thunk checkout contains unsaved modifications"
649
745
650 -- Check whether there are any stashes
746 -- Check whether there are any stashes
@@ -657,7 +753,7 @@ getThunkPtr' checkClean thunkDir = do
657 ] ++ T.lines stashOutput
753 ] ++ T.lines stashOutput
658 True -> return ()
754 True -> return ()
659
755
660− -- Get current branch ``
756+ -- Get current branch
661 (mCurrentBranch, mCurrentCommit) <- do
757 (mCurrentBranch, mCurrentCommit) <- do
662 b <- listToMaybe
758 b <- listToMaybe
663 <$> T.lines
759 <$> T.lines
@@ -666,7 +762,7 @@ getThunkPtr' checkClean thunkDir = do
666 <$> T.lines
762 <$> T.lines
667 <$> readGitProcess thunkDir ["rev-parse", "HEAD"]
763 <$> readGitProcess thunkDir ["rev-parse", "HEAD"]
668 case b of
764 case b of
669− (Just "HEAD") -> failWith $ T.unlines $
765+ (Just "HEAD") -> failWith $ T.unlines
670 [ "thunk pack: You are in 'detached HEAD' state."
766 [ "thunk pack: You are in 'detached HEAD' state."
671 , "If you want to pack at the current ref \
767 , "If you want to pack at the current ref \
672 \then please create a new branch with 'git checkout -b <new-branch-name>' and push this upstream."
768 \then please create a new branch with 'git checkout -b <new-branch-name>' and push this upstream."
@@ -738,7 +834,7 @@ getThunkPtr' checkClean thunkDir = do
738 [ "thunk pack: Certain branches in the thunk have commits not yet pushed upstream:"
834 [ "thunk pack: Certain branches in the thunk have commits not yet pushed upstream:"
739 , ""
835 , ""
740 ] ++
836 ] ++
741− (flip map (Map.toList nonGood) $ \(branch, (upstream, (ahead, behind))) -> mconcat
837+ flip map (Map.toList nonGood) (\(branch, (upstream, (ahead, behind))) -> mconcat
742 [" ", branch, " ahead: ", T.pack (show ahead), " behind: ", T.pack (show behind), " remote branch ", upstream]) ++
838 [" ", branch, " ahead: ", T.pack (show ahead), " behind: ", T.pack (show behind), " remote branch ", upstream]) ++
743 [ ""
839 [ ""
744 , "Please push these upstream and try again. (Or just fetch, if they are somehow \
840 , "Please push these upstream and try again. (Or just fetch, if they are somehow \
@@ -746,7 +842,7 @@ getThunkPtr' checkClean thunkDir = do
746 ]
842 ]
747
843
748 -- We assume it's safe to pack the thunk at this point
844 -- We assume it's safe to pack the thunk at this point
749− putLog Informational $ "All changes safe in git remotes. OK to pack thunk."
845+ putLog Informational "All changes safe in git remotes. OK to pack thunk."
750
846
751 let remote = maybe "origin" snd $ flip Map.lookup headUpstream =<< mCurrentBranch
847 let remote = maybe "origin" snd $ flip Map.lookup headUpstream =<< mCurrentBranch
752
848
@@ -759,7 +855,7 @@ getThunkPtr' checkClean thunkDir = do
759 remoteUri <- case parseGitUri remoteUri' of
855 remoteUri <- case parseGitUri remoteUri' of
760 Nothing -> failWith $ "Could not identify git remote: " <> remoteUri'
856 Nothing -> failWith $ "Could not identify git remote: " <> remoteUri'
761 Just uri -> pure uri
857 Just uri -> pure uri
762− uriThunkPtr remoteUri mCurrentBranch mCurrentCommit
858+ uriThunkPtr remoteUri mPrivate mCurrentBranch mCurrentCommit
763
859
764-- | Get the latest revision available from the given source
860-- | Get the latest revision available from the given source
765getLatestRev :: MonadObelisk m => ThunkSource -> m ThunkRev
861getLatestRev :: MonadObelisk m => ThunkSource -> m ThunkRev
@@ -778,18 +874,18 @@ getLatestRev os = do
778-- performance. If that doesn't work (e.g. authentication issue), we fall back
874-- performance. If that doesn't work (e.g. authentication issue), we fall back
779-- on just doing things the normal way for git repos in general, and save it as
875-- on just doing things the normal way for git repos in general, and save it as
780-- a regular git thunk.
876-- a regular git thunk.
781−uriThunkPtr :: MonadObelisk m => URI -> Maybe Text -> Maybe Text -> m ThunkPtr
877+uriThunkPtr :: MonadObelisk m => GitUri -> Maybe Bool -> Maybe Text -> Maybe Text -> m ThunkPtr
782−uriThunkPtr uri mbranch mcommit = do
878+uriThunkPtr uri mPrivate mbranch mcommit = do
783 commit <- case mcommit of
879 commit <- case mcommit of
784 Nothing -> gitGetCommitBranch uri mbranch >>= return . snd
880 Nothing -> gitGetCommitBranch uri mbranch >>= return . snd
785 (Just c) -> return c
881 (Just c) -> return c
786− (src, rev) <- case uriToThunkSource uri mbranch of
882+ (src, rev) <- uriToThunkSource uri mPrivate mbranch >>= \case
787 ThunkSource_GitHub s -> do
883 ThunkSource_GitHub s -> do
788 rev <- runExceptT $ githubThunkRev s commit
884 rev <- runExceptT $ githubThunkRev s commit
789 case rev of
885 case rev of
790 Right r -> pure (ThunkSource_GitHub s, r)
886 Right r -> pure (ThunkSource_GitHub s, r)
791 Left e -> do
887 Left e -> do
792− putLog Warning $ "\
888+ putLog Warning "\
793\Failed to fetch archive from GitHub. This is probably a private repo. \
889\Failed to fetch archive from GitHub. This is probably a private repo. \
794\Falling back on normal fetchgit. Original failure:"
890\Falling back on normal fetchgit. Original failure:"
795 errorToWarning e
891 errorToWarning e
@@ -806,34 +902,66 @@ uriThunkPtr uri mbranch mcommit = do
806-- If the thunk is a GitHub thunk and fails, we do *not* fall back like with
902-- If the thunk is a GitHub thunk and fails, we do *not* fall back like with
807-- `uriThunkPtr`. Unlike a plain URL, a thunk src explicitly states which method
903-- `uriThunkPtr`. Unlike a plain URL, a thunk src explicitly states which method
808-- should be employed, and so we respect that.
904-- should be employed, and so we respect that.
809−uriToThunkSource :: URI -> Maybe Text -> ThunkSource
905+uriToThunkSource :: MonadObelisk m => GitUri -> Maybe Bool -> Maybe Text -> m ThunkSource
810−uriToThunkSource u
906+uriToThunkSource (GitUri u) mPrivate
811− | Right uriAuth <- uriAuthority u
907+ | Right uriAuth <- URI.uriAuthority u
812− , Just scheme <- unRText <$> uriScheme u
908+ , Just scheme <- URI.unRText <$> URI.uriScheme u
813 , case scheme of
909 , case scheme of
814− "ssh" -> uriAuth == Authority
910+ "ssh" -> uriAuth == URI.Authority
815− { authUserInfo = Just $ UserInfo (fromRight' $ mkUsername "git") Nothing
911+ { URI.authUserInfo = Just $ URI.UserInfo (fromRight' $ URI.mkUsername "git") Nothing
816− , authHost = fromRight' $ mkHost "github.com"
912+ , URI.authHost = fromRight' $ URI.mkHost "github.com"
817− , authPort = Nothing
913+ , URI.authPort = Nothing
818 }
914 }
819 s -> s `L.elem` [ "git", "https", "http" ] -- "http:" just redirects to "https:"
915 s -> s `L.elem` [ "git", "https", "http" ] -- "http:" just redirects to "https:"
820− && unRText (authHost uriAuth) == "github.com"
916+ && URI.unRText (URI.authHost uriAuth) == "github.com"
821− , Just (_, owner :| [repoish]) <- uriPath u
917+ , Just (_, owner :| [repoish]) <- URI.uriPath u
822− = \mbranch -> ThunkSource_GitHub $ GitHubSource
918+ = \mbranch -> do
823− { _gitHubSource_owner = N $ unRText owner
919+ isPrivate <- getIsPrivate
824− , _gitHubSource_repo = N $ let
920+ pure $ ThunkSource_GitHub $ GitHubSource
825− repoish' = unRText repoish
921+ { _gitHubSource_owner = N $ URI.unRText owner
826− in case T.stripSuffix ".git" repoish' of
922+ , _gitHubSource_repo = N $ let
827− Just repo -> repo
923+ repoish' = URI.unRText repoish
828− Nothing -> repoish'
924+ in fromMaybe repoish' $ T.stripSuffix ".git" repoish'
829− , _gitHubSource_branch = N <$> mbranch
925+ , _gitHubSource_branch = N <$> mbranch
830− }
926+ , _gitHubSource_private = isPrivate
927+ }
831
928
832− | otherwise = \mbranch -> ThunkSource_Git $ GitSource
929+ | otherwise = \mbranch -> do
833− { _gitSource_url = u
930+ isPrivate <- getIsPrivate
834− , _gitSource_branch = N <$> mbranch
931+ pure $ ThunkSource_Git $ GitSource
835− , _gitSource_fetchSubmodules = False -- TODO: How do we determine if this should be true?
932+ { _gitSource_url = GitUri u
836− }
933+ , _gitSource_branch = N <$> mbranch
934+ , _gitSource_fetchSubmodules = False -- TODO: How do we determine if this should be true?
935+ , _gitSource_private = isPrivate
936+ }
937+ where
938+ getIsPrivate = maybe (guessGitRepoIsPrivate $ GitUri u) pure mPrivate
939+
940+guessGitRepoIsPrivate :: MonadObelisk m => GitUri -> m Bool
941+guessGitRepoIsPrivate uri = flip fix urisToTry $ \loop -> \case
942+ [] -> pure True
943+ uriAttempt:xs -> do
944+ result <- readCreateProcessWithExitCode $
945+ isolateGitProc $
946+ gitProcNoRepo
947+ [ "ls-remote"
948+ , "--quiet"
949+ , "--exit-code"
950+ , "--symref"
951+ , T.unpack $ gitUriToText uriAttempt
952+ ]
953+ case result of
954+ (ExitSuccess, _, _) -> pure False -- Must be a public repo
955+ _ -> loop xs
956+ where
957+ urisToTry = nubOrd $
958+ -- Include the original URI if it isn't using SSH because SSH will certainly fail.
959+ [uri | fmap URI.unRText (URI.uriScheme (unGitUri uri)) /= Just "ssh"] <>
960+ [changeScheme "https" uri, changeScheme "http" uri]
961+ changeScheme scheme (GitUri u) = GitUri $ u
962+ { URI.uriScheme = URI.mkScheme scheme
963+ , URI.uriAuthority = (\x -> x { URI.authUserInfo = Nothing }) <$> URI.uriAuthority u
964+ }
837
965
838-- Funny signature indicates no effects depend on the optional branch name.
966-- Funny signature indicates no effects depend on the optional branch name.
839githubThunkRev
967githubThunkRev
@@ -845,19 +973,19 @@ githubThunkRev
845githubThunkRev s commit = do
973githubThunkRev s commit = do
846 owner <- forcePP $ _gitHubSource_owner s
974 owner <- forcePP $ _gitHubSource_owner s
847 repo <- forcePP $ _gitHubSource_repo s
975 repo <- forcePP $ _gitHubSource_repo s
848− revTarball <- mkPathPiece $ commit <> ".tar.gz"
976+ revTarball <- URI.mkPathPiece $ commit <> ".tar.gz"
849− let archiveUri = URI
977+ let archiveUri = GitUri $ URI.URI
850− { uriScheme = Just $ fromRight' $ mkScheme "https"
978+ { URI.uriScheme = Just $ fromRight' $ URI.mkScheme "https"
851− , uriAuthority = Right $ Authority
979+ , URI.uriAuthority = Right $ URI.Authority
852− { authUserInfo = Nothing
980+ { URI.authUserInfo = Nothing
853− , authHost = fromRight' $ mkHost "github.com"
981+ , URI.authHost = fromRight' $ URI.mkHost "github.com"
854− , authPort = Nothing
982+ , URI.authPort = Nothing
855 }
983 }
856− , uriPath = Just ( False
984+ , URI.uriPath = Just ( False
857− , owner :| [ repo, fromRight' $ mkPathPiece "archive", revTarball ]
985+ , owner :| [ repo, fromRight' $ URI.mkPathPiece "archive", revTarball ]
858 )
986 )
859− , uriQuery = []
987+ , URI.uriQuery = []
860− , uriFragment = Nothing
988+ , URI.uriFragment = Nothing
861 }
989 }
862 hash <- getNixSha256ForUriUnpacked archiveUri
990 hash <- getNixSha256ForUriUnpacked archiveUri
863 putLog Debug $ "Nix sha256 is " <> hash
991 putLog Debug $ "Nix sha256 is " <> hash
@@ -866,8 +994,8 @@ githubThunkRev s commit = do
866 , _thunkRev_nixSha256 = hash
994 , _thunkRev_nixSha256 = hash
867 }
995 }
868 where
996 where
869− forcePP :: Name entity -> m (RText 'PathPiece)
997+ forcePP :: Name entity -> m (URI.RText 'URI.PathPiece)
870− forcePP = mkPathPiece . untagName
998+ forcePP = URI.mkPathPiece . untagName
871
999
872gitThunkRev
1000gitThunkRev
873 :: MonadObelisk m
1001 :: MonadObelisk m
@@ -876,11 +1004,11 @@ gitThunkRev
876 -> m ThunkRev
1004 -> m ThunkRev
877gitThunkRev s commit = do
1005gitThunkRev s commit = do
878 let u = _gitSource_url s
1006 let u = _gitSource_url s
879− protocols = ["https", "ssh", "git"]
1007+ protocols = ["file", "https", "ssh", "git"]
880− Just scheme <- pure $ unRText <$> uriScheme u
1008+ scheme = maybe "file" URI.unRText $ URI.uriScheme $ (\(GitUri x) -> x) u
881 unless (T.toLower scheme `elem` protocols) $
1009 unless (T.toLower scheme `elem` protocols) $
882 failWith $ "obelisk currently only supports "
1010 failWith $ "obelisk currently only supports "
883− <> T.intercalate ", " protocols <> " protocols for non-GitHub remotes"
1011+ <> T.intercalate ", " protocols <> " protocols for plain Git remotes"
884 hash <- nixPrefetchGit u commit $ _gitSource_fetchSubmodules s
1012 hash <- nixPrefetchGit u commit $ _gitSource_fetchSubmodules s
885 putLog Informational $ "Nix sha256 is " <> hash
1013 putLog Informational $ "Nix sha256 is " <> hash
886 pure $ ThunkRev
1014 pure $ ThunkRev
@@ -893,11 +1021,12 @@ gitThunkRev s commit = do
893--
1021--
894-- If the branch name is passed in, it is returned exactly as-is. If it is not
1022-- If the branch name is passed in, it is returned exactly as-is. If it is not
895-- passed it, the default branch of the repo is used instead.
1023-- passed it, the default branch of the repo is used instead.
1024+
896gitGetCommitBranch
1025gitGetCommitBranch
897− :: MonadObelisk m => URI -> Maybe Text -> m (Text, CommitId)
1026+ :: MonadObelisk m => GitUri -> Maybe Text -> m (Text, CommitId)
898gitGetCommitBranch uri mbranch = withExitFailMessage ("Failure for git remote " <> uriMsg) $ do
1027gitGetCommitBranch uri mbranch = withExitFailMessage ("Failure for git remote " <> uriMsg) $ do
899 (_, bothMaps) <- gitLsRemote
1028 (_, bothMaps) <- gitLsRemote
900− (T.unpack $ render uri)
1029+ (T.unpack $ gitUriToText uri)
901 (GitRef_Branch <$> mbranch)
1030 (GitRef_Branch <$> mbranch)
902 Nothing
1031 Nothing
903 branch <- case mbranch of
1032 branch <- case mbranch of
@@ -913,18 +1042,21 @@ gitGetCommitBranch uri mbranch = withExitFailMessage ("Failure for git remote "
913 pure (branch, commit)
1042 pure (branch, commit)
914 where
1043 where
915 rethrowE = either failWith pure
1044 rethrowE = either failWith pure
916− uriMsg = render uri
1045+ uriMsg = gitUriToText uri
1046+
1047+parseGitUri :: Text -> Maybe GitUri
1048+parseGitUri x = GitUri <$> (parseFileURI x <|> parseAbsoluteURI x <|> parseSshShorthand x)
917
1049
918−parseGitUri :: Text -> Maybe URI
1050+parseFileURI :: Text -> Maybe URI.URI
919−parseGitUri x = parseAbsoluteURI x <|> parseSshShorthand x
1051+parseFileURI uri = if "/" `T.isPrefixOf` uri then parseAbsoluteURI ("file://" <> uri) else Nothing
920
1052
921−parseAbsoluteURI :: Text -> Maybe URI
1053+parseAbsoluteURI :: Text -> Maybe URI.URI
922parseAbsoluteURI uri = do
1054parseAbsoluteURI uri = do
923− parsedUri <- mkURI uri
1055+ parsedUri <- URI.mkURI uri
924− guard $ isPathAbsolute parsedUri
1056+ guard $ URI.isPathAbsolute parsedUri
925− pure $ parsedUri
1057+ pure parsedUri
926
1058
927−parseSshShorthand :: Text -> Maybe URI
1059+parseSshShorthand :: Text -> Maybe URI.URI
928parseSshShorthand uri = do
1060parseSshShorthand uri = do
929 -- This is what git does to check that the remote
1061 -- This is what git does to check that the remote
930 -- is not a local file path when parsing shorthand.
1062 -- is not a local file path when parsing shorthand.
@@ -937,4 +1069,4 @@ parseSshShorthand uri = do
937 -- This check is used to disambiguate a filepath containing a colon from shorthand
1069 -- This check is used to disambiguate a filepath containing a colon from shorthand
938 guard $ isNothing (T.findIndex (=='/') authAndHostname)
1070 guard $ isNothing (T.findIndex (=='/') authAndHostname)
939 && not (T.null colonAndPath)
1071 && not (T.null colonAndPath)
940− mkURI properUri
1072+ URI.mkURI properUri
lib/command/src/Obelisk/Command/Upgrade/Hash.hsdeleted−114
@@ -1,114 +0,0 @@
1−{-# LANGUAGE FlexibleContexts #-}
2−{-# LANGUAGE LambdaCase #-}
3−{-# LANGUAGE OverloadedStrings #-}
4−{-# LANGUAGE ScopedTypeVariables #-}
5−{-# LANGUAGE TupleSections #-}
6−-- | Hash a git repo or arbitrary directory using nix-hash.
7−module Obelisk.Command.Upgrade.Hash
8− ( getDirectoryHash
9− , getHashAtGitRevision
10− ) where
11−
12−import Control.Monad (forM, forM_)
13−import Control.Monad.IO.Class (liftIO)
14−import Data.Bool (bool)
15−import Data.Maybe (catMaybes)
16−import Data.Semigroup ((<>))
17−import Data.Text (Text)
18−import qualified Data.Text as T
19−import System.Directory
20−import System.FilePath
21−import System.IO.Temp
22−import System.Process (cwd, proc)
23−
24−import Obelisk.App (MonadObelisk)
25−import Obelisk.CliApp
26−import Obelisk.Command.Utils
27−
28−import Obelisk.Migration
29−
30−-- | Get the unique hash of the given directory
31−--
32−-- Excludes the following before computing the hash:
33−-- * the specified top-level files/ directories.
34−-- * .git directory
35−-- * untracked Git files
36−-- * ignored Git files
37−-- * empty directories
38−--
39−-- Uses the same predictive algorithm as that of Nix (`nix-hash`).
40−--
41−-- This function will do a full copy of the directory to a temporary location before
42−-- computing the hash. Because it will be deleting the files in exclude list, and
43−-- other files if the directory is a git repo, which needs to be done as `nix-hash`
44−-- doesn't support taking an excludes list.
45−getDirectoryHash :: MonadObelisk m => [FilePath] -> FilePath -> m Hash
46−getDirectoryHash excludes dir = withSystemTempDirectory "obelisk-hash-" $ \tmpDir -> do
47− withSpinnerNoTrail (T.pack $ "Copying " <> dir <> " to " <> tmpDir) $ do
48− runProc $ copyDir dir tmpDir
49− getDirectoryHashDestructive excludes tmpDir
50−
51−getDirectoryHashDestructive :: MonadObelisk m => [FilePath] -> FilePath -> m Hash
52−getDirectoryHashDestructive excludes dir = do
53− liftIO (doesDirectoryExist $ dir </> ".git") >>= \case
54− True -> do
55− tidyUpGitWorkingCopy dir
56− withSpinnerNoTrail "Removing .git directory" $
57− liftIO $ removePathForcibly $ dir </> ".git"
58− False -> pure ()
59− withSpinnerNoTrail "Removing excluded paths" $ do
60− forM_ (fmap (dir </>) excludes) $
61− liftIO . removePathForcibly
62− nixHash dir
63−
64−getHashAtGitRevision :: MonadObelisk m => [Text] -> [FilePath] -> FilePath -> m [Hash]
65−getHashAtGitRevision revs excludes dir = withSystemTempDirectory "obelisk-hashrev-" $ \tmpDir -> do
66− withSpinnerNoTrail (T.pack $ "Copying " <> dir <> " to " <> tmpDir) $ do
67− runProc $ copyDir dir tmpDir
68− tidyUpGitWorkingCopy tmpDir
69− -- Discard changes to tracked files
70− runProcSilently $ gitProc tmpDir ["reset", "--hard"]
71− forM revs $ \rev -> do
72− runProcSilently $ gitProc tmpDir ["checkout", T.unpack rev]
73− -- Checking out an arbitrary revision can leave untracked files (from
74− -- previous revison) around, so tidy them up.
75− tidyUpGitWorkingCopy tmpDir
76− withFilesStashed tmpDir (excludes <> [".git"]) $
77− nixHash tmpDir
78− where
79− withFilesStashed base fs m = withSystemTempDirectory "obelisk-hashrev-stash-" $ \stashDir -> do
80− existingPaths <- fmap catMaybes $ forM fs $ \p -> do
81− liftIO (doesPathExist $ base </> p) >>= pure . bool Nothing (Just p)
82− forM_ existingPaths $ \p ->
83− liftIO $ renamePath (base </> p) (stashDir </> p)
84− result <- m
85− forM_ existingPaths $ \p ->
86− liftIO $ renamePath (stashDir </> p) (base </> p)
87− return result
88−
89−nixHash :: MonadObelisk m => FilePath -> m Hash
90−nixHash dir = withSpinnerNoTrail "Running `nix-hash`" $
91− readProc $ proc "nix-hash" [dir]
92−
93−-- | Clean up the following files in the git working copy
94−--
95−-- * Paths ignored by .gitignore, and still present
96−-- * Untracked files (not added to git index)
97−-- * Any empty directories (these are not tracked by git)
98−--
99−-- Note that this leaves modified (staged or unstaged) files as they are.
100−tidyUpGitWorkingCopy :: MonadObelisk m => FilePath -> m ()
101−tidyUpGitWorkingCopy dir = withSpinnerNoTrail "Tidying up git working copy" $ do
102− ignored <- gitLsFiles dir ["--ignored", "--exclude-standard", "--others"]
103− untracked <- gitLsFiles dir ["--exclude-standard", "--others"]
104− putLog Debug $ "Ignored: " <> T.pack (show $ length ignored) <> " files."
105− putLog Debug $ "Untracked files:\n" <> T.unlines untracked
106− withSpinnerNoTrail "Removing untracked and ignored files" $ do
107− forM_ (fmap ((dir </>) . T.unpack) $ ignored <> untracked) $
108− liftIO . removePathForcibly
109− -- Empty directories won't be included in these lists. Git doesn't track them
110− -- So we must delete these separately.
111− runProc $ proc "find" [dir, "-depth", "-empty", "-type", "d", "-delete"]
112− where
113− gitLsFiles pwd opts = fmap T.lines $ readProc $
114− (proc "git" $ ["ls-files", "."] <> opts) { cwd = Just pwd }
lib/command/src/Obelisk/Command/Utils.hs+43−18
@@ -4,6 +4,7 @@
4{-# LANGUAGE ScopedTypeVariables #-}
4{-# LANGUAGE ScopedTypeVariables #-}
5{-# LANGUAGE OverloadedStrings #-}
5{-# LANGUAGE OverloadedStrings #-}
6{-# LANGUAGE PatternGuards #-}
6{-# LANGUAGE PatternGuards #-}
7+{-# LANGUAGE TemplateHaskell #-}
7module Obelisk.Command.Utils where
8module Obelisk.Command.Utils where
8
9
9import Control.Applicative hiding (many)
10import Control.Applicative hiding (many)
@@ -20,18 +21,31 @@ import Data.Maybe (maybeToList)
20import Data.Text (Text)
21import Data.Text (Text)
21import qualified Data.Text as T
22import qualified Data.Text as T
22import Data.Void (Void)
23import Data.Void (Void)
23−import System.Directory (canonicalizePath)
24−import System.Environment (getExecutablePath)
25import System.Exit (ExitCode)
24import System.Exit (ExitCode)
26−import qualified System.Process as P
25+import System.Which (staticWhich)
27import Text.Megaparsec as MP
26import Text.Megaparsec as MP
28import Text.Megaparsec.Char as MP
27import Text.Megaparsec.Char as MP
29
28
30import Obelisk.App (MonadObelisk)
29import Obelisk.App (MonadObelisk)
31import Obelisk.CliApp
30import Obelisk.CliApp
32
31
33−getObeliskExe :: IO FilePath
32+cp :: FilePath
34−getObeliskExe = getExecutablePath >>= canonicalizePath
33+cp = $(staticWhich "cp")
34+
35+ghcidExePath :: FilePath
36+ghcidExePath = $(staticWhich "ghcid")
37+
38+findExePath :: FilePath
39+findExePath = $(staticWhich "find")
40+
41+nixExePath :: FilePath
42+nixExePath = $(staticWhich "nix")
43+
44+nixBuildExePath :: FilePath
45+nixBuildExePath = $(staticWhich "nix-build")
46+
47+jreKeyToolPath :: FilePath
48+jreKeyToolPath = $(staticWhich "keytool")
35
49
36-- Check whether the working directory is clean
50-- Check whether the working directory is clean
37checkGitCleanStatus :: MonadObelisk m => FilePath -> Bool -> m Bool
51checkGitCleanStatus :: MonadObelisk m => FilePath -> Bool -> m Bool
@@ -61,20 +75,31 @@ initGit repo = do
61 git ["add", "."]
75 git ["add", "."]
62 git ["commit", "-m", "Initial commit."]
76 git ["commit", "-m", "Initial commit."]
63
77
64−gitProcNoRepo :: [String] -> P.CreateProcess
78+gitProcNoRepo :: [String] -> ProcessSpec
65−gitProcNoRepo = P.proc "git"
79+gitProcNoRepo args = setEnvOverride (M.singleton "GIT_TERMINAL_PROMPT" "0" <>) $ proc "git" args
66
80
67−gitProc :: FilePath -> [String] -> P.CreateProcess
81+gitProc :: FilePath -> [String] -> ProcessSpec
68gitProc repo = gitProcNoRepo . runGitInDir
82gitProc repo = gitProcNoRepo . runGitInDir
69 where
83 where
70 runGitInDir args' = case filter (not . null) args' of
84 runGitInDir args' = case filter (not . null) args' of
71 args@("clone":_) -> args <> [repo]
85 args@("clone":_) -> args <> [repo]
72 args -> ["-C", repo] <> args
86 args -> ["-C", repo] <> args
73
87
74−-- | Recursively copy a directory using `cp -a`
88+isolateGitProc :: ProcessSpec -> ProcessSpec
75−copyDir :: FilePath -> FilePath -> P.CreateProcess
89+isolateGitProc = setEnvOverride (overrides <>)
90+ where
91+ overrides = M.fromList
92+ [ ("HOME", "/dev/null")
93+ , ("GIT_CONFIG_NOSYSTEM", "1")
94+ , ("GIT_TERMINAL_PROMPT", "0") -- git 2.3+
95+ , ("GIT_ASKPASS", "echo") -- pre git 2.3 to just use empty password
96+ , ("GIT_SSH_COMMAND", "ssh -o PreferredAuthentications password -o PubkeyAuthentication no -o GSSAPIAuthentication no")
97+ ]
98+
99+-- | Recursively copy a directory using `cp -a` -- TODO: Should use -rT instead of -a
100+copyDir :: FilePath -> FilePath -> ProcessSpec
76copyDir src dest =
101copyDir src dest =
77− (P.proc "cp" ["-a", ".", dest]) { P.cwd = Just src }
102+ setCwd (Just src) $ proc cp ["-a", ".", dest] -- TODO: This will break if dest is relative since we change cwd
78
103
79readGitProcess :: MonadObelisk m => FilePath -> [String] -> m Text
104readGitProcess :: MonadObelisk m => FilePath -> [String] -> m Text
80readGitProcess repo = readProcessAndLogOutput (Debug, Notice) . gitProc repo
105readGitProcess repo = readProcessAndLogOutput (Debug, Notice) . gitProc repo
@@ -87,15 +112,15 @@ processToShellString cmd args = unwords $ map quoteAndEscape (cmd : args)
87 where quoteAndEscape x = T.unpack $ "'" <> T.replace "'" "'\''" (T.pack x) <> "'"
112 where quoteAndEscape x = T.unpack $ "'" <> T.replace "'" "'\''" (T.pack x) <> "'"
88
113
89-- | A simpler wrapper for CliApp's most used process function with sensible defaults.
114-- | A simpler wrapper for CliApp's most used process function with sensible defaults.
90−runProc :: MonadObelisk m => P.CreateProcess -> m ()
115+runProc :: MonadObelisk m => ProcessSpec -> m ()
91runProc = callProcessAndLogOutput (Notice, Error)
116runProc = callProcessAndLogOutput (Notice, Error)
92
117
93-- | Like runProc, but all output goes to Debug logging level
118-- | Like runProc, but all output goes to Debug logging level
94−runProcSilently :: MonadObelisk m => P.CreateProcess -> m ()
119+runProcSilently :: MonadObelisk m => ProcessSpec -> m ()
95runProcSilently = callProcessAndLogOutput (Debug, Debug)
120runProcSilently = callProcessAndLogOutput (Debug, Debug)
96
121
97-- | A simpler wrapper for CliApp's readProcessAndLogStderr with sensible defaults.
122-- | A simpler wrapper for CliApp's readProcessAndLogStderr with sensible defaults.
98−readProc :: MonadObelisk m => P.CreateProcess -> m Text
123+readProc :: MonadObelisk m => ProcessSpec -> m Text
99readProc = readProcessAndLogOutput (Debug, Error)
124readProc = readProcessAndLogOutput (Debug, Error)
100
125
101tshow :: Show a => a -> Text
126tshow :: Show a => a -> Text
@@ -129,13 +154,13 @@ gitLsRemote repository mRef mBranch = do
129 (exitCode, out, _err) <- case mBranch of
154 (exitCode, out, _err) <- case mBranch of
130 Nothing -> readCreateProcessWithExitCode $ gitProcNoRepo $
155 Nothing -> readCreateProcessWithExitCode $ gitProcNoRepo $
131 ["ls-remote", "--exit-code", "--symref", repository]
156 ["ls-remote", "--exit-code", "--symref", repository]
132− ++ (maybeToList $ T.unpack . showGitRef <$> mRef)
157+ ++ maybeToList (T.unpack . showGitRef <$> mRef)
133− Just branchName -> readCreateProcessWithExitCode $ gitProcNoRepo $
158+ Just branchName -> readCreateProcessWithExitCode $ gitProcNoRepo
134 ["ls-remote", "--exit-code", repository, branchName]
159 ["ls-remote", "--exit-code", repository, branchName]
135 let t = T.pack out
160 let t = T.pack out
136 maps <- case MP.runParser parseLsRemote "" t of
161 maps <- case MP.runParser parseLsRemote "" t of
137− Left err -> failWith $ T.pack $ MP.parseErrorPretty' t err
162+ Left err -> failWith $ T.pack $ MP.errorBundlePretty err
138− Right table -> pure $ bimap M.fromList M.fromList $ partitionEithers $ table
163+ Right table -> pure $ bimap M.fromList M.fromList $ partitionEithers table
139 putLog Debug $ "git ls-remote maps: " <> T.pack (show maps)
164 putLog Debug $ "git ls-remote maps: " <> T.pack (show maps)
140 pure (exitCode, maps)
165 pure (exitCode, maps)
141
166
lib/command/src/Obelisk/Command/VmBuilder.hs+2−2
@@ -18,7 +18,6 @@ import System.Directory (createDirectoryIfMissing)
18import System.Exit (ExitCode(..))
18import System.Exit (ExitCode(..))
19import System.FilePath ((<.>), (</>))
19import System.FilePath ((<.>), (</>))
20import qualified System.Info
20import qualified System.Info
21−import System.Process (proc)
22
21
23import Obelisk.App (MonadObelisk, getObeliskUserStateDir)
22import Obelisk.App (MonadObelisk, getObeliskUserStateDir)
24import Obelisk.CliApp
23import Obelisk.CliApp
@@ -156,7 +155,8 @@ testLinuxBuild stateDir
156 | System.Info.os == "linux" = failWith "Using the docker builder is not necessary on linux."
155 | System.Info.os == "linux" = failWith "Using the docker builder is not necessary on linux."
157 | otherwise = do
156 | otherwise = do
158 (exitCode, _stdout, stderr) <- readCreateProcessWithExitCode $ proc "nix-build"
157 (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"
160 , "--builders", nixBuildersArgString stateDir
160 , "--builders", nixBuildersArgString stateDir
161 ]
161 ]
162 unless (exitCode == ExitSuccess) $ do
162 unless (exitCode == ExitSuccess) $ do
lib/executable-config/default.nix+6−3
@@ -4,7 +4,7 @@
4# platform.
4# platform.
5{ lib
5{ lib
6, runCommand
6, runCommand
7−, cleanHaskellSource
7+, obeliskCleanSource
8}:
8}:
9
9
10let
10let
@@ -23,7 +23,10 @@ let
23in
23in
24
24
25{
25{
26− haskellPackage = self: self.callPackage (cleanHaskellSource ./lookup) {};
26+ haskellOverlay = self: super: {
27+ obelisk-executable-config-lookup = self.callCabal2nix "obelisk-executable-config-lookup" (obeliskCleanSource ./lookup) {};
28+ };
29+
27 platforms = {
30 platforms = {
28 android = {
31 android = {
29 # Inject the given config directory into an android assets folder
32 # Inject the given config directory into an android assets folder
@@ -34,7 +37,7 @@ in
34 inject = injectConfig;
37 inject = injectConfig;
35 };
38 };
36 web = {
39 web = {
37− inject = self: self.callCabal2nix "obelisk-executable-config-inject" (cleanHaskellSource ./inject) {};
40+ inject = self: self.callCabal2nix "obelisk-executable-config-inject" (obeliskCleanSource ./inject) {};
38 };
41 };
39 };
42 };
40}
43}
lib/executable-config/inject/obelisk-executable-config-inject.cabal+2−2
@@ -9,8 +9,8 @@ library
9 build-depends:
9 build-depends:
10 base,
10 base,
11 bytestring,
11 bytestring,
12− directory,
12+ base64-bytestring,
13− filepath,
13+ containers,
14 reflex-dom-core,
14 reflex-dom-core,
15 text
15 text
16
16
lib/executable-config/inject/src/Obelisk/ExecutableConfig/Inject.hs+14−40
@@ -2,50 +2,24 @@
2module Obelisk.ExecutableConfig.Inject where
2module Obelisk.ExecutableConfig.Inject where
3
3
4import Control.Monad (mapM_)
4import Control.Monad (mapM_)
5−import Control.Monad.IO.Class (MonadIO, liftIO)
5+import Control.Monad.IO.Class (MonadIO)
6−import Data.Semigroup ((<>))
7import Data.ByteString (ByteString)
6import Data.ByteString (ByteString)
7+import qualified Data.ByteString.Base64 as B64
8+import Data.Map (Map)
9+import qualified Data.Map as Map
10+import Data.Semigroup ((<>))
8import Data.Text (Text)
11import Data.Text (Text)
9−import qualified Data.Text as T
12+import qualified Data.Text.Encoding as T
10−import qualified Data.Text.IO as T
11import Reflex.Dom.Core hiding (value)
13import Reflex.Dom.Core hiding (value)
12−import System.Directory
13−import System.FilePath ((</>))
14−
15−-- | Produces a @<script>@ tag containing the contents of the config item,
16−-- rendered as a 'ByteString'.
17−inject :: FilePath -> IO ByteString
18−inject key = do
19− value <- T.readFile key
20− fmap snd $ renderStatic $ injectPure (T.pack key) value
21
14
22-- | Produces a @<script>@ tag with an @id@ attribute containing the key and
15-- | Produces a @<script>@ tag with an @id@ attribute containing the key and
23−-- whose value is the provided configuration
16+-- whose value is the provided configuration, encoded in base64.
24−injectPure :: DomBuilder t m => Text -> Text -> m ()
17+injectPure :: DomBuilder t m => Text -> ByteString -> m ()
25injectPure key value =
18injectPure key value =
26− let attrs = ("type" =: "text/plain" <> "id" =: ("config-" <> key))
19+ let attrs = ("type" =: "text/plain" <> "data-obelisk-executable-config-inject-key" =: key <> "data-hydration-skip" =: "")
27− in elAttr "script" attrs $ text value
20+ in elAttr "script" attrs $ text (T.decodeUtf8 (B64.encode value))
28−
29−-- | Scans the "common" and "frontend" configuration folders and produces
30−-- injectable @<script>@ tags containing the configuration keys (filepaths) and
31−-- values.
32−injectExecutableConfigs :: (MonadIO m, DomBuilder t m) => m ()
33−injectExecutableConfigs = do
34− cfgC <- getConfigs "config/common"
35− cfgF <- getConfigs "config/frontend"
36− mapM_ (uncurry injectPure) (cfgC <> cfgF)
37
21
38−getConfigs :: MonadIO m => FilePath -> m [(Text, Text)]
22+-- | Produces injectable @<script>@ tags containing the configuration keys
39−getConfigs fp = liftIO $ do
23+-- (filepaths) and values.
40− dir <- doesDirectoryExist fp
24+injectExecutableConfigs :: (MonadIO m, DomBuilder t m) => Map Text ByteString -> m ()
41− if dir
25+injectExecutableConfigs = mapM_ (uncurry injectPure) . Map.toList
42− then do
43− ps <- listDirectory fp
44− fmap concat $ mapM (\p -> getConfigs $ fp </> p) ps
45− else do
46− file <- doesFileExist fp
47− if file
48− then do
49− f <- T.readFile fp
50− return [(T.pack fp, f)]
51− else return []
lib/executable-config/lookup/default.nixdeleted−24
@@ -1,24 +0,0 @@
1−{ hostPlatform, ghc
2−, mkDerivation, base, bytestring, filepath, stdenv, text, transformers
3−, android-activity ? null, jsaddle-wkwebview ? null, ghcjs-dom ? null
4−}:
5−let isAndroid = hostPlatform.libc == "bionic";
6− isIOS = hostPlatform.isDarwin && hostPlatform.isAarch64;
7− isGhcjs = ghc.isGhcjs or false;
8−in mkDerivation {
9− pname = "obelisk-executable-config";
10− version = "0.1";
11− src = ./.;
12− libraryHaskellDepends = [
13− base bytestring filepath text transformers
14− ] ++ (if isAndroid then [
15− android-activity
16− ] else if isIOS then [
17− jsaddle-wkwebview
18− ] else if isGhcjs then [
19− ghcjs-dom
20− ] else [
21− ]);
22− license = stdenv.lib.licenses.bsd3;
23−}
24−
lib/executable-config/lookup/obelisk-executable-config-lookup.cabalnew file+45
@@ -0,0 +1,45 @@
1+name: obelisk-executable-config-lookup
2+version: 0.1.1
3+cabal-version: >= 1.2
4+build-type: Simple
5+
6+library
7+ exposed-modules: Obelisk.ExecutableConfig.Lookup, Obelisk.Configs
8+ if os(ios)
9+ hs-source-dirs: src-ios, src
10+ other-modules: Obelisk.Configs.Internal.Directory
11+ build-depends: jsaddle-wkwebview
12+ build-depends: directory
13+ else
14+ if os(linux-android)
15+ hs-source-dirs: src-android, src
16+ c-sources: cbits/ExecutableConfig.c
17+ other-modules: Obelisk.ExecutableConfig.Internal.AssetManager
18+ build-depends: android-activity
19+ ld-options: -landroid
20+ else
21+ if impl(ghcjs)
22+ hs-source-dirs: src-ghcjs, src
23+ build-depends: ghcjs-dom
24+ else
25+ hs-source-dirs: src-other, src
26+ other-modules: Obelisk.Configs.Internal.Directory
27+ build-depends: directory
28+
29+ build-depends:
30+ base,
31+ base64-bytestring,
32+ bytestring,
33+ containers,
34+ filepath,
35+ text,
36+ transformers,
37+ transformers-base,
38+ ref-tf,
39+ primitive,
40+ monad-control,
41+ reflex,
42+ reflex-dom,
43+ jsaddle
44+
45+ ghc-options: -Wall
lib/executable-config/lookup/obelisk-executable-config.cabaldeleted−30
@@ -1,30 +0,0 @@
1−name: obelisk-executable-config
2−version: 0.1.1
3−cabal-version: >= 1.2
4−build-type: Simple
5−
6−library
7− exposed-modules: Obelisk.ExecutableConfig
8− if os(ios)
9− hs-source-dirs: src-ios
10− build-depends: jsaddle-wkwebview
11− else
12− if os(linux-android)
13− hs-source-dirs: src-android
14− c-sources: cbits/ExecutableConfig.c
15− other-modules: Obelisk.ExecutableConfig.Internal.AssetManager
16− build-depends: android-activity
17− ld-options: -landroid
18− else
19− if impl(ghcjs)
20− hs-source-dirs: src-ghcjs
21− build-depends: ghcjs-dom
22− else
23− hs-source-dirs: src
24−
25− build-depends:
26− base,
27− bytestring,
28− filepath,
29− text,
30− transformers
lib/executable-config/lookup/src-android/Obelisk/ExecutableConfig.hsdeleted−27
@@ -1,27 +0,0 @@
1−module Obelisk.ExecutableConfig (get) where
2−
3−import Control.Exception (bracket)
4−import qualified Data.ByteString as BS
5−import Data.Text (Text)
6−import qualified Data.Text as T
7−import qualified Data.Text.Encoding as T
8−import Foreign.C.String (withCString)
9−import Foreign.Ptr (nullPtr)
10−import System.FilePath.Posix ((</>))
11−
12−import Obelisk.ExecutableConfig.Internal.AssetManager
13−
14−get :: Text -> IO (Maybe Text)
15−get name = bracket getAssets freeAssetManager $ \mgrObj -> do
16− mgr <- assetManagerFromJava mgrObj
17− let open = do
18− a <- withCString (T.unpack name) $ \fn ->
19− assetManager_open mgr fn 3
20− return $ if unAAsset a == nullPtr
21− then Nothing
22− else Just a
23− close = mapM_ asset_close
24− bracket open close $ mapM $ \asset -> do
25− b <- asset_getBuffer asset
26− l <- asset_getLength asset
27− fmap T.decodeUtf8 $ BS.packCStringLen (b, fromIntegral l)
lib/executable-config/lookup/src-android/Obelisk/ExecutableConfig/Lookup.hsnew file+60
@@ -0,0 +1,60 @@
1+{-# LANGUAGE LambdaCase #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+module Obelisk.ExecutableConfig.Lookup where
4+
5+import Control.Exception (bracket)
6+import Control.Monad (forM)
7+import Data.ByteString (ByteString)
8+import qualified Data.ByteString as BS
9+import Data.Map (Map)
10+import qualified Data.Map as Map
11+import Data.Text (Text)
12+import qualified Data.Text.Encoding as T
13+import Foreign.C.String (withCString)
14+import Foreign.Ptr (nullPtr)
15+
16+import Obelisk.ExecutableConfig.Internal.AssetManager
17+
18+getFromMgr :: AAssetManager -> BS.ByteString -> IO (Maybe ByteString)
19+getFromMgr mgr name = do
20+ let open = do
21+ a <- BS.useAsCString name $ \fn ->
22+ assetManager_open mgr fn 3
23+ return $ if unAAsset a == nullPtr
24+ then Nothing
25+ else Just a
26+ close = mapM_ asset_close
27+ bracket open close $ mapM $ \asset -> do
28+ b <- asset_getBuffer asset
29+ l <- asset_getLength asset
30+ BS.packCStringLen (b, fromIntegral l)
31+
32+getConfigs :: IO (Map Text ByteString)
33+getConfigs = fmap (Map.mapKeys T.decodeUtf8) $ bracket getAssets freeAssetManager $ \mgrObj -> do
34+ mgr <- assetManagerFromJava mgrObj
35+ let openDir = do
36+ d <- withCString "config.files" $ \fn ->
37+ assetManager_open mgr fn 3
38+ return $ if unAAsset d == nullPtr
39+ then Nothing
40+ else Just d
41+ closeDir = mapM_ asset_close
42+ configPaths <- bracket openDir closeDir $ \case
43+ Just asset -> do
44+ b <- asset_getBuffer asset
45+ l <- asset_getLength asset
46+ lines0 <$> BS.packCStringLen (b, fromIntegral l)
47+ Nothing -> error "could not open configuration manifest 'config.files'"
48+ result <- fmap Map.fromList $ forM configPaths $ \fp ->
49+ getFromMgr mgr ("config/" <> fp) >>= \case
50+ Just v -> return (fp, v)
51+ Nothing -> error $ "Config present in config.files but not in assets: " <> show fp
52+ putStrLn $ "getConfigs: found " <> show result
53+ pure result
54+
55+lines0 :: BS.ByteString -> [BS.ByteString]
56+lines0 ps
57+ | BS.null ps = []
58+ | otherwise = case BS.elemIndex 0 ps of
59+ Nothing -> [ps]
60+ Just n -> BS.take n ps : lines0 (BS.drop (n+1) ps)
lib/executable-config/lookup/src-ghcjs/Obelisk/ExecutableConfig.hsdeleted−17
@@ -1,17 +0,0 @@
1−{-# LANGUAGE OverloadedStrings #-}
2−module Obelisk.ExecutableConfig where
3−
4−import Control.Monad.Trans.Maybe
5−import Data.Semigroup ((<>))
6−import Data.Text (Text)
7−import GHCJS.DOM
8−import GHCJS.DOM.Document
9−import GHCJS.DOM.Element
10−import GHCJS.DOM.NonElementParentNode
11−
12−get :: Text -> IO (Maybe Text)
13−get item = runMaybeT $ do
14− doc <- MaybeT currentDocument
15− e <- MaybeT $ getElementById doc $ "config-" <> item
16− getInnerHTML e
17−
lib/executable-config/lookup/src-ghcjs/Obelisk/ExecutableConfig/Lookup.hsnew file+51
@@ -0,0 +1,51 @@
1+{-# LANGUAGE LambdaCase #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+module Obelisk.ExecutableConfig.Lookup where
4+
5+import Data.ByteString (ByteString)
6+import qualified Data.ByteString.Base64 as B64
7+import Data.Map (Map)
8+import qualified Data.Map as Map
9+import Data.Maybe (catMaybes)
10+import Data.Text (Text)
11+import qualified Data.Text.Encoding as T
12+import Data.Traversable (for)
13+import GHCJS.DOM
14+import GHCJS.DOM.Document (getHead)
15+import GHCJS.DOM.DOMStringMap (get)
16+import GHCJS.DOM.Element (getInnerHTML)
17+import GHCJS.DOM.HTMLElement (HTMLElement(HTMLElement), getDataset)
18+import GHCJS.DOM.NodeList (IsNodeList, item, getLength)
19+import GHCJS.DOM.ParentNode (querySelectorAll)
20+import GHCJS.DOM.Types (MonadJSM, Node(Node), castTo)
21+
22+getConfigs :: IO (Map Text ByteString)
23+getConfigs = do
24+ Just doc <- currentDocument
25+ Just hd <- getHead doc
26+ nodes <- nodeListNodes =<< querySelectorAll hd ("[data-obelisk-executable-config-inject-key]" :: Text)
27+ fmap Map.fromList $ for nodes $ \node -> do
28+ e <- castTo HTMLElement node >>= \case
29+ Nothing -> error "Found node with data attribute obelisk-executable-config-inject-key that is not an HTMLElement."
30+ Just htmlE -> return htmlE
31+ dataset <- getDataset e
32+ (,)
33+ -- the key is camelCased: https://html.spec.whatwg.org/multipage/dom.html#dom-dataset
34+ <$> get dataset ("obeliskExecutableConfigInjectKey" :: Text)
35+ <*> (fmap decodeOrFail (getInnerHTML e))
36+ where
37+ decodeOrFail x = case B64.decode (T.encodeUtf8 x) of
38+ Left e -> error ("Obelisk.ExecutableConfig.Lookup.getConfigs: error when decoding base64: " ++ e)
39+ 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-ios/Obelisk/ExecutableConfig.hsdeleted−22
@@ -1,22 +0,0 @@
1−module Obelisk.ExecutableConfig (get) where
2−
3−import Control.Exception
4−import Control.Monad
5−import Control.Monad.Trans.Maybe
6−import Data.ByteString as BS
7−import Data.Text (Text)
8−import qualified Data.Text as T
9−import qualified Data.Text.IO as T
10−import qualified Data.Text.Encoding as T
11−import Language.Javascript.JSaddle.WKWebView
12−import System.FilePath.Posix ((</>))
13−import System.IO.Error
14−
15−get :: Text -> IO (Maybe Text)
16−get name = fmap join $ mainBundleResourcePath >>= \mp -> forM mp $ \p ->
17− catchDoesNotExist $ T.readFile $ T.unpack (T.decodeUtf8 p) </> T.unpack name
18−
19−catchDoesNotExist :: IO a -> IO (Maybe a)
20−catchDoesNotExist f = catchJust doesNotExist (Just <$> f) $ const $ return Nothing
21− where
22− doesNotExist e = if isDoesNotExistError e then Just () else Nothing
lib/executable-config/lookup/src-ios/Obelisk/ExecutableConfig/Lookup.hsnew file+17
@@ -0,0 +1,17 @@
1+{-# LANGUAGE LambdaCase #-}
2+module Obelisk.ExecutableConfig.Lookup where
3+
4+import Data.ByteString (ByteString)
5+import Data.Map (Map)
6+import Data.Text (Text)
7+import qualified Data.Text as T
8+import qualified Data.Text.Encoding as T
9+import Language.Javascript.JSaddle.WKWebView
10+import System.FilePath.Posix
11+
12+import Obelisk.Configs.Internal.Directory
13+
14+getConfigs :: IO (Map Text ByteString)
15+getConfigs = mainBundleResourcePath >>= \case
16+ Nothing -> error "Could not get bundle resource path."
17+ Just p -> getConfigsFromDirectory $ T.unpack (T.decodeUtf8 p) </> "config"
lib/executable-config/lookup/src-other/Obelisk/ExecutableConfig/Lookup.hsnew file+11
@@ -0,0 +1,11 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+module Obelisk.ExecutableConfig.Lookup where
3+
4+import Data.Map (Map)
5+import Data.Text (Text)
6+import Data.ByteString (ByteString)
7+
8+import Obelisk.Configs.Internal.Directory (getConfigsFromDirectory)
9+
10+getConfigs :: IO (Map Text ByteString)
11+getConfigs = getConfigsFromDirectory "config"
lib/executable-config/lookup/src/Obelisk/Configs.hsnew file+129
@@ -0,0 +1,129 @@
1+{-# LANGUAGE CPP #-}
2+{-# LANGUAGE DataKinds #-}
3+{-# LANGUAGE DefaultSignatures #-}
4+{-# LANGUAGE DeriveFunctor #-}
5+{-# LANGUAGE FlexibleContexts #-}
6+{-# LANGUAGE FlexibleInstances #-}
7+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
8+{-# LANGUAGE MultiParamTypeClasses #-}
9+{-# LANGUAGE OverloadedStrings #-}
10+{-# LANGUAGE RankNTypes #-}
11+{-# LANGUAGE TypeFamilies #-}
12+{-# LANGUAGE UndecidableInstances #-}
13+module Obelisk.Configs
14+ ( HasConfigs(..)
15+ , ConfigsT
16+ , runConfigsT
17+ , mapConfigsT
18+ , getTextConfig
19+ ) where
20+
21+import Control.Applicative
22+import Control.Monad
23+import Control.Monad.Base
24+import Control.Monad.Fix
25+import Control.Monad.Primitive
26+import Control.Monad.Ref
27+import Control.Monad.IO.Class
28+import Control.Monad.Trans.Class
29+import Control.Monad.Trans.Control
30+import Control.Monad.Trans.Reader
31+import Data.ByteString (ByteString)
32+import Data.Map (Map)
33+import qualified Data.Map as Map
34+import Data.Text (Text)
35+import qualified Data.Text.Encoding as T
36+import Reflex
37+import Reflex.Host.Class
38+import Reflex.Dom
39+#ifndef ghcjs_HOST_OS
40+import Language.Javascript.JSaddle (MonadJSM)
41+#endif
42+
43+class Monad m => HasConfigs m where
44+ getConfigs :: m (Map Text ByteString)
45+ default getConfigs :: (HasConfigs m', m ~ t m', MonadTrans t) => m (Map Text ByteString)
46+ getConfigs = lift getConfigs
47+ getConfig :: Text -> m (Maybe ByteString)
48+ getConfig k = do
49+ configs <- getConfigs
50+ return $ Map.lookup k configs
51+
52+instance Monad m => HasConfigs (ConfigsT m) where
53+ getConfigs = ConfigsT ask
54+
55+getTextConfig :: HasConfigs m => Text -> m (Maybe Text)
56+getTextConfig k = fmap T.decodeUtf8 <$> getConfig k
57+
58+instance HasConfigs m => HasConfigs (BehaviorWriterT t w m)
59+instance HasConfigs m => HasConfigs (DynamicWriterT t w m)
60+instance HasConfigs m => HasConfigs (EventWriterT t w m)
61+instance HasConfigs m => HasConfigs (PostBuildT t m)
62+instance HasConfigs m => HasConfigs (QueryT t q m)
63+instance HasConfigs m => HasConfigs (ReaderT r m)
64+instance HasConfigs m => HasConfigs (RequesterT t request response m)
65+instance HasConfigs m => HasConfigs (StaticDomBuilderT t m)
66+instance HasConfigs m => HasConfigs (TriggerEventT t m)
67+
68+newtype ConfigsT m a = ConfigsT { unConfigsT :: ReaderT (Map Text ByteString) m a }
69+ deriving
70+ ( Functor
71+ , Applicative
72+ , Monad
73+ , MonadPlus
74+ , Alternative
75+ , MonadFix
76+ , MonadIO
77+ , MonadBase m'
78+ , MonadBaseControl m'
79+ , MonadRef
80+ , MonadTrans
81+ , DomBuilder t
82+ , MonadHold t
83+ , MonadReflexCreateTrigger t
84+ , MonadSample t
85+ , NotReady t
86+ , PostBuild t
87+ , TriggerEvent t
88+ , HasDocument
89+ , DomRenderHook t
90+ , HasJSContext
91+ , HasJS js
92+#ifndef ghcjs_HOST_OS
93+ , MonadJSM
94+#endif
95+ )
96+
97+instance PerformEvent t m => PerformEvent t (ConfigsT m) where
98+ type Performable (ConfigsT m) = ConfigsT (Performable m)
99+ performEvent e = ConfigsT $ ReaderT $ \configs ->
100+ performEvent $ runConfigsT configs <$> e
101+ performEvent_ e = ConfigsT $ ReaderT $ \configs ->
102+ performEvent_ $ runConfigsT configs <$> e
103+
104+instance Adjustable t m => Adjustable t (ConfigsT m) where
105+ runWithReplace a e = ConfigsT $ runWithReplace (unConfigsT a) (unConfigsT <$> e)
106+ traverseDMapWithKeyWithAdjust f m e = ConfigsT $ traverseDMapWithKeyWithAdjust (\k v -> unConfigsT $ f k v) m e
107+ traverseIntMapWithKeyWithAdjust f m e = ConfigsT $ traverseIntMapWithKeyWithAdjust (\k v -> unConfigsT $ f k v) m e
108+ traverseDMapWithKeyWithAdjustWithMove f m e = ConfigsT $ traverseDMapWithKeyWithAdjustWithMove (\k v -> unConfigsT $ f k v) m e
109+
110+instance Prerender js t m => Prerender js t (ConfigsT m) where
111+ type Client (ConfigsT m) = ConfigsT (Client m)
112+ prerender server client = ConfigsT $ ReaderT $ \configs ->
113+ prerender (runConfigsT configs server) (runConfigsT configs client)
114+
115+instance PrimMonad m => PrimMonad (ConfigsT m) where
116+ type PrimState (ConfigsT m) = PrimState m
117+ primitive = lift . primitive
118+
119+runConfigsT
120+ :: Map Text ByteString
121+ -> ConfigsT m a
122+ -> m a
123+runConfigsT cs child = runReaderT (unConfigsT child) cs
124+
125+mapConfigsT
126+ :: (forall x. m x -> n x)
127+ -> ConfigsT m a
128+ -> ConfigsT n a
129+mapConfigsT f (ConfigsT x) = ConfigsT $ mapReaderT f x
lib/executable-config/lookup/src/Obelisk/Configs/Internal/Directory.hsnew file+24
@@ -0,0 +1,24 @@
1+{-# LANGUAGE LambdaCase #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+module Obelisk.Configs.Internal.Directory where
4+
5+import Control.Monad
6+import Data.ByteString (ByteString)
7+import qualified Data.ByteString as BS
8+import Data.Map (Map)
9+import qualified Data.Map as Map
10+import Data.Text (Text)
11+import qualified Data.Text as T
12+import System.Directory
13+import System.FilePath.Posix
14+
15+getConfigsFromDirectory :: FilePath -> IO (Map Text ByteString)
16+getConfigsFromDirectory base = doesDirectoryExist base >>= \case
17+ True -> do
18+ ps <- listDirectory base
19+ fmap mconcat $ forM ps $ \p -> do
20+ subdirConfigs <- getConfigsFromDirectory $ base </> p
21+ pure $ Map.mapKeys (T.pack . (p </>) . T.unpack) subdirConfigs
22+ False -> doesFileExist base >>= \case
23+ True -> Map.singleton "" <$> BS.readFile base
24+ False -> pure mempty
lib/executable-config/lookup/src/Obelisk/ExecutableConfig.hsdeleted−13
@@ -1,13 +0,0 @@
1−module Obelisk.ExecutableConfig (get) where
2−
3−import Control.Exception
4−import Data.Text (Text)
5−import Data.Text as T
6−import Data.Text.IO as T
7−import System.FilePath.Posix ((</>))
8−import System.IO.Error
9−
10−get :: Text -> IO (Maybe Text)
11−get path = do
12− let doesNotExist = \e -> if isDoesNotExistError e then Just () else Nothing
13− catchJust doesNotExist (fmap Just $ T.readFile $ T.unpack path) (\_ -> pure Nothing)
lib/frontend/obelisk-frontend.cabal+8−1
@@ -7,10 +7,15 @@ library
7 hs-source-dirs: src
7 hs-source-dirs: src
8 build-depends: base,
8 build-depends: base,
9 bytestring,
9 bytestring,
10+ containers,
11+ cookie,
10 dependent-sum,
12 dependent-sum,
11 ghcjs-dom,
13 ghcjs-dom,
12 jsaddle,
14 jsaddle,
13 lens,
15 lens,
16+ mtl,
17+ obelisk-executable-config-inject,
18+ obelisk-executable-config-lookup,
14 obelisk-route,
19 obelisk-route,
15 primitive,
20 primitive,
16 ref-tf,
21 ref-tf,
@@ -18,5 +23,7 @@ library
18 reflex-dom-core,
23 reflex-dom-core,
19 text,
24 text,
20 transformers
25 transformers
21− exposed-modules: Obelisk.Frontend
26+ exposed-modules:
27+ Obelisk.Frontend
28+ Obelisk.Frontend.Cookie
22 ghc-options: -Wall
29 ghc-options: -Wall
lib/frontend/src/Obelisk/Frontend.hs+182−96
@@ -1,10 +1,13 @@
1+{-# LANGUAGE CPP #-}
1{-# LANGUAGE ConstraintKinds #-}
2{-# LANGUAGE ConstraintKinds #-}
2{-# LANGUAGE FlexibleContexts #-}
3{-# LANGUAGE FlexibleContexts #-}
3{-# LANGUAGE FlexibleInstances #-}
4{-# LANGUAGE FlexibleInstances #-}
4{-# LANGUAGE LambdaCase #-}
5{-# LANGUAGE LambdaCase #-}
5{-# LANGUAGE MultiParamTypeClasses #-}
6{-# LANGUAGE MultiParamTypeClasses #-}
6{-# LANGUAGE OverloadedStrings #-}
7{-# LANGUAGE OverloadedStrings #-}
8+{-# LANGUAGE PackageImports #-}
7{-# LANGUAGE RankNTypes #-}
9{-# LANGUAGE RankNTypes #-}
10+{-# LANGUAGE RecursiveDo #-}
8{-# LANGUAGE ScopedTypeVariables #-}
11{-# LANGUAGE ScopedTypeVariables #-}
9{-# LANGUAGE TemplateHaskell #-}
12{-# LANGUAGE TemplateHaskell #-}
10{-# LANGUAGE TypeFamilies #-}
13{-# LANGUAGE TypeFamilies #-}
@@ -13,39 +16,55 @@ module Obelisk.Frontend
13 ( ObeliskWidget
16 ( ObeliskWidget
14 , Frontend (..)
17 , Frontend (..)
15 , runFrontend
18 , runFrontend
19+ , runFrontendWithConfigsAndCurrentRoute
16 , renderFrontendHtml
20 , renderFrontendHtml
21+ , removeHTMLConfigs
22+ , FrontendMode (..)
23+ , FrontendWidgetT
24+ , module Obelisk.Frontend.Cookie
17 ) where
25 ) where
18
26
19import Prelude hiding ((.))
27import Prelude hiding ((.))
20
28
21import Control.Category
29import Control.Category
22−import Control.Concurrent
23import Control.Lens
30import Control.Lens
24−import Control.Monad
25import Control.Monad.Fix
31import Control.Monad.Fix
26import Control.Monad.IO.Class
32import Control.Monad.IO.Class
27import Control.Monad.Primitive
33import Control.Monad.Primitive
34+import Control.Monad.Reader
28import Control.Monad.Ref
35import Control.Monad.Ref
29−import Control.Monad.Trans.Class
36+import Control.Monad.Trans.Maybe (MaybeT(..), runMaybeT)
30import Data.ByteString (ByteString)
37import Data.ByteString (ByteString)
31−import Data.Dependent.Sum (DSum (..))
38+import Data.Foldable (for_)
32import Data.Functor.Sum
39import Data.Functor.Sum
33−import Data.IORef
40+import Data.Map (Map)
41+import Data.Maybe (catMaybes)
34import Data.Monoid ((<>))
42import Data.Monoid ((<>))
35import Data.Text (Text)
43import Data.Text (Text)
36−import GHCJS.DOM hiding (bracket, catch)
44+import qualified GHCJS.DOM as DOM
37−import GHCJS.DOM.Document
38−import GHCJS.DOM.Node
39import qualified GHCJS.DOM.Types as DOM
45import qualified GHCJS.DOM.Types as DOM
40−import Language.Javascript.JSaddle (JSM)
46+import qualified GHCJS.DOM.History as DOM
47+import qualified GHCJS.DOM.Window as DOM
48+import Language.Javascript.JSaddle (MonadJSM, JSM, jsNull)
49+import GHCJS.DOM (currentDocument)
50+import "ghcjs-dom" GHCJS.DOM.Document (getHead)
51+import GHCJS.DOM.Node (Node, removeChild_)
52+import GHCJS.DOM.NodeList (IsNodeList, item, getLength)
53+import GHCJS.DOM.ParentNode (querySelectorAll)
54+import Obelisk.Frontend.Cookie
41import Obelisk.Route.Frontend
55import Obelisk.Route.Frontend
42import Reflex.Dom.Core
56import Reflex.Dom.Core
43import Reflex.Host.Class
57import Reflex.Host.Class
44−import qualified Reflex.TriggerEvent.Base as TriggerEvent
58+import Obelisk.Configs
59+import Obelisk.ExecutableConfig.Inject (injectExecutableConfigs)
60+import qualified Obelisk.ExecutableConfig.Lookup as Lookup
61+import Web.Cookie
62+
63+import Debug.Trace
45
64
46makePrisms ''Sum
65makePrisms ''Sum
47
66
48−type ObeliskWidget t x route m =
67+type ObeliskWidget js t route m =
49 ( DomBuilder t m
68 ( DomBuilder t m
50 , MonadFix m
69 , MonadFix m
51 , MonadHold t m
70 , MonadHold t m
@@ -53,8 +72,6 @@ type ObeliskWidget t x route m =
53 , MonadReflexCreateTrigger t m
72 , MonadReflexCreateTrigger t m
54 , PostBuild t m
73 , PostBuild t m
55 , PerformEvent t m
74 , PerformEvent t m
56− , MonadIO m
57− , MonadIO (Performable m)
58 , TriggerEvent t m
75 , TriggerEvent t m
59 , HasDocument m
76 , HasDocument m
60 , MonadRef m
77 , MonadRef m
@@ -63,106 +80,175 @@ type ObeliskWidget t x route m =
63 , Ref (Performable m) ~ Ref IO
80 , Ref (Performable m) ~ Ref IO
64 , MonadFix (Performable m)
81 , MonadFix (Performable m)
65 , PrimMonad m
82 , PrimMonad m
66− , Prerender x m
83+ , Prerender js t m
67− , SetRoute t route m
84+ , PrebuildAgnostic t route m
85+ , PrebuildAgnostic t route (Client m)
86+ , HasConfigs m
87+ , HasCookies m
88+ , MonadIO (Performable m)
89+ )
90+
91+type PrebuildAgnostic t route m =
92+ ( SetRoute t route m
68 , RouteToUrl route m
93 , RouteToUrl route m
94+ , MonadFix m
95+ , HasConfigs m
96+ , HasConfigs (Performable m)
69 )
97 )
70
98
71data Frontend route = Frontend
99data Frontend route = Frontend
72− { _frontend_head :: !(forall t m x. ObeliskWidget t x route m => RoutedT t route m ())
100+ { _frontend_head :: !(forall js t m. ObeliskWidget js t route m => RoutedT t route m ())
73− , _frontend_body :: !(forall t m x. ObeliskWidget t x route m => RoutedT t route m ())
101+ , _frontend_body :: !(forall js t m. ObeliskWidget js t route m => RoutedT t route m ())
74 }
102 }
75
103
76−type Widget' x = ImmediateDomBuilderT DomTimeline (DomCoreWidget x)
104+baseTag :: forall route js t m. ObeliskWidget js t route m => RoutedT t route m ()
77−
105+baseTag = elAttr "base" ("href" =: "/") blank --TODO: Figure out the base URL from the routes
78−-- | A widget that isn't attached to any particular part of the DOM hierarchy
106+
79−type FloatingWidget x = TriggerEventT DomTimeline (DomCoreWidget x)
107+removeHTMLConfigs :: JSM ()
80−
108+removeHTMLConfigs = void $ runMaybeT $ do
81−type DomCoreWidget x = PostBuildT DomTimeline (WithJSContextSingleton x (PerformEventT DomTimeline DomHost))
109+ doc <- MaybeT currentDocument
82−
110+ hd <- MaybeT $ getHead doc
83−--TODO: Rename
111+ es <- nodeListNodes =<< querySelectorAll hd ("[data-obelisk-executable-config-inject-key]" :: Text)
84−{-# INLINABLE attachWidget''' #-}
112+ for_ es $ removeChild_ hd
85−attachWidget'''
113+
86− :: (EventChannel -> PerformEventT DomTimeline DomHost (IORef (Maybe (EventTrigger DomTimeline ()))))
114+-- | Collect all nodes in the node list.
87− -> IO ()
115+--
88−attachWidget''' w = runDomHost $ do
116+-- TODO: this and the version in exe-config/ghcjs/lookup should be
89− events <- liftIO newChan
117+-- upstreamed to jsaddle.
90− (postBuildTriggerRef, fc@(FireCommand fire)) <- hostPerformEventT $ w events
118+nodeListNodes :: (IsNodeList l, MonadJSM m) => l -> m [Node]
91− mPostBuildTrigger <- readRef postBuildTriggerRef
119+nodeListNodes es = do
92− forM_ mPostBuildTrigger $ \postBuildTrigger -> fire [postBuildTrigger :=> Identity ()] $ return ()
120+ len <- getLength es
93− liftIO $ processAsyncEvents events fc
121+ -- Warning! len is unsigned. If the NodeList is empty, we must avoid
94−
122+ -- accidentally traversing over [0..maxBound::Word]
95−--TODO: This is a collection of random stuff; we should make it make some more sense and then upstream to reflex-dom-core
123+ nodes <- traverse (item es) $ if len == 0 then [] else [0..len-1]
96−runWithHeadAndBody
124+ pure $ catMaybes nodes
97− :: ( (forall c. Widget' () c -> FloatingWidget () c) -- "Append to head"
125+
98− -> (forall c. Widget' () c -> FloatingWidget () c) -- "Append to body"
126+setInitialRoute :: Bool -> JSM ()
99− -> FloatingWidget () ()
127+setInitialRoute useHash = do
100− )
128+ traceM "setInitialRoute"
129+ window <- DOM.currentWindowUnchecked
130+ initialLocation <- DOM.getLocation window
131+ initialUri <- getLocationUri initialLocation
132+ history <- DOM.getHistory window
133+ DOM.replaceState history jsNull ("" :: Text) $ Just $
134+ show $ setAdaptedUriPath useHash "/" initialUri
135+
136+data FrontendMode = FrontendMode
137+ { _frontendMode_hydrate :: Bool
138+ -- ^ There is already a rendering of the DOM in place; hydrate it rather
139+ -- than building new DOM
140+ , _frontendMode_adjustRoute :: Bool
141+ -- ^ The page can't use regular routes, so encode routes into the hash
142+ -- instead
143+ }
144+
145+-- | Run the frontend, setting the initial route to "/" on platforms where no
146+-- route exists ambiently in the context (e.g. anything but web).
147+-- Selects FrontendMode based on platform; this doesn't work for jsaddle-warp
148+runFrontend
149+ :: forall backendRoute route
150+ . Encoder Identity Identity (R (FullRoute backendRoute route)) PageName
151+ -> Frontend (R route)
101 -> JSM ()
152 -> JSM ()
102−runWithHeadAndBody app = withJSContextSingletonMono $ \jsSing -> do
103− globalDoc <- currentDocumentUnchecked
104− headFragment <- createDocumentFragment globalDoc
105− bodyFragment <- createDocumentFragment globalDoc
106− unreadyChildren <- liftIO $ newIORef 0
107− let commit = do
108− headElement <- getHeadUnchecked globalDoc
109− bodyElement <- getBodyUnchecked globalDoc
110− void $ inAnimationFrame' $ \_ -> do
111− replaceElementContents headElement headFragment
112− replaceElementContents bodyElement bodyFragment
113− liftIO $ attachWidget''' $ \events -> flip runWithJSContextSingleton jsSing $ do
114− (postBuild, postBuildTriggerRef) <- newEventWithTriggerRef
115− let appendImmediateDom :: DOM.DocumentFragment -> Widget' () c -> FloatingWidget () c
116− appendImmediateDom df w = do
117− events' <- TriggerEvent.askEvents
118− lift $ do
119− doc <- getOwnerDocumentUnchecked df
120− let builderEnv = ImmediateDomBuilderEnv
121− { _immediateDomBuilderEnv_document = doc
122− , _immediateDomBuilderEnv_parent = toNode df
123− , _immediateDomBuilderEnv_unreadyChildren = unreadyChildren
124− , _immediateDomBuilderEnv_commitAction = commit
125− }
126− runImmediateDomBuilderT w builderEnv events'
127− flip runPostBuildT postBuild $ flip runTriggerEventT events $ app (appendImmediateDom headFragment) (appendImmediateDom bodyFragment)
128− liftIO (readIORef unreadyChildren) >>= \case
129− 0 -> DOM.liftJSM commit
130− _ -> return ()
131− return postBuildTriggerRef
132−
133−runFrontend :: forall backendRoute route. Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName -> Frontend (R route) -> JSM ()
134runFrontend validFullEncoder frontend = do
153runFrontend validFullEncoder frontend = do
135− let ve = validFullEncoder . hoistParse errorLeft (prismEncoder (rPrism $ _InR . _ObeliskRoute_App))
154+ let mode = FrontendMode
155+ { _frontendMode_hydrate =
156+#ifdef ghcjs_HOST_OS
157+ True
158+#else
159+ False
160+#endif
161+ , _frontendMode_adjustRoute =
162+#ifdef ghcjs_HOST_OS
163+ False
164+#else
165+ True
166+#endif
167+ }
168+ configs <- liftIO Lookup.getConfigs
169+ when (_frontendMode_hydrate mode) removeHTMLConfigs
170+ -- There's no fundamental reason that adjustRoute needs to control setting the
171+ -- initial route and *also* the useHash parameter; that's why these are
172+ -- separate here. However, currently, they are always the same.
173+ when (_frontendMode_adjustRoute mode) $ do
174+ setInitialRoute $ _frontendMode_adjustRoute mode
175+ runFrontendWithConfigsAndCurrentRoute mode configs validFullEncoder frontend
176+
177+runFrontendWithConfigsAndCurrentRoute
178+ :: forall backendRoute frontendRoute
179+ . FrontendMode
180+ -> Map Text ByteString
181+ -> Encoder Identity Identity (R (FullRoute backendRoute frontendRoute)) PageName
182+ -> Frontend (R frontendRoute)
183+ -> JSM ()
184+runFrontendWithConfigsAndCurrentRoute mode configs validFullEncoder frontend = do
185+ let ve = validFullEncoder . hoistParse errorLeft (prismEncoder (rPrism $ _FullRoute_Frontend . _ObeliskRoute_App))
136 errorLeft = \case
186 errorLeft = \case
137 Left _ -> error "runFrontend: Unexpected non-app ObeliskRoute reached the frontend. This shouldn't happen."
187 Left _ -> error "runFrontend: Unexpected non-app ObeliskRoute reached the frontend. This shouldn't happen."
138 Right x -> Identity x
188 Right x -> Identity x
139− runMyRouteViewT
189+ w :: ( RawDocument (DomBuilderSpace (HydrationDomBuilderT s DomTimeline m)) ~ DOM.Document
140− :: ( TriggerEvent t m
190+ , Ref (Performable m) ~ Ref IO
141− , PerformEvent t m
191+ , Ref m ~ Ref IO
142− , MonadHold t m
192+ , DomBuilder DomTimeline (HydrationDomBuilderT s DomTimeline m)
193+ , MonadHold DomTimeline m
194+ , MonadRef m
195+ , MonadRef (Performable m)
196+ , MonadReflexCreateTrigger DomTimeline m
197+ , PerformEvent DomTimeline m
198+ , PostBuild DomTimeline m
199+ , PrimMonad m
200+ , MonadSample DomTimeline (Performable m)
143 , DOM.MonadJSM m
201 , DOM.MonadJSM m
144− , DOM.MonadJSM (Performable m)
202+ , MonadFix (Client (HydrationDomBuilderT s DomTimeline m))
145− , MonadFix m
146 , MonadFix (Performable m)
203 , MonadFix (Performable m)
204+ , MonadFix m
205+ , Prerender js DomTimeline (HydrationDomBuilderT s DomTimeline m)
206+ , MonadIO (Performable m)
147 )
207 )
148− => RoutedT t (R route) (SetRouteT t (R route) (RouteToUrlT (R route) m)) a
208+ => (forall c. HydrationDomBuilderT s DomTimeline m c -> FloatingWidget () c)
149− -> m a
209+ -> (forall c. HydrationDomBuilderT s DomTimeline m c -> FloatingWidget () c)
150− runMyRouteViewT = runRouteViewT ve
210+ -> FloatingWidget () ()
151− runWithHeadAndBody $ \appendHead appendBody -> runMyRouteViewT $ do
211+ w appendHead appendBody = do
152− mapRoutedT (mapSetRouteT (mapRouteToUrlT appendHead)) $ _frontend_head frontend
212+ rec switchover <- runRouteViewT ve switchover (_frontendMode_adjustRoute mode) $ do
153− mapRoutedT (mapSetRouteT (mapRouteToUrlT appendBody)) $ _frontend_body frontend
213+ (switchover'', fire) <- newTriggerEvent
214+ mapRoutedT (mapSetRouteT (mapRouteToUrlT (appendHead . runConfigsT configs))) $ do
215+ -- The order here is important - baseTag has to be before headWidget!
216+ baseTag
217+ _frontend_head frontend
218+ mapRoutedT (mapSetRouteT (mapRouteToUrlT (appendBody . runConfigsT configs))) $ do
219+ _frontend_body frontend
220+ switchover' <- case _frontendMode_hydrate mode of
221+ True -> lift $ lift $ lift $ lift $ HydrationDomBuilderT $ asks _hydrationDomBuilderEnv_switchover
222+ False -> getPostBuild
223+ performEvent_ $ liftIO (fire ()) <$ switchover'
224+ pure switchover''
225+ pure ()
226+ if _frontendMode_hydrate mode
227+ then runHydrationWidgetWithHeadAndBody (pure ()) w
228+ else runImmediateWidgetWithHeadAndBody w
229+
230+type FrontendWidgetT r = RoutedT DomTimeline r (SetRouteT DomTimeline r (RouteToUrlT r (ConfigsT (CookiesT (HydratableT (PostBuildT DomTimeline (StaticDomBuilderT DomTimeline (PerformEventT DomTimeline DomHost))))))))
154
231
155renderFrontendHtml
232renderFrontendHtml
156− :: (t ~ DomTimeline)
233+ :: MonadIO m
157− => (r' -> Text)
234+ => Map Text ByteString
235+ -> Cookies
236+ -> (r -> Text)
158 -> r
237 -> r
159− -> RoutedT t r (SetRouteT t r' (RouteToUrlT r' (PostBuildT DomTimeline (StaticDomBuilderT DomTimeline (PerformEventT DomTimeline DomHost))))) ()
238+ -> Frontend r
160− -> RoutedT t r (SetRouteT t r' (RouteToUrlT r' (PostBuildT DomTimeline (StaticDomBuilderT DomTimeline (PerformEventT DomTimeline DomHost))))) ()
239+ -> FrontendWidgetT r ()
161− -> IO ByteString
240+ -> FrontendWidgetT r ()
162−renderFrontendHtml urlEnc route headWidget bodyWidget = do
241+ -> m ByteString
242+renderFrontendHtml configs cookies urlEnc route frontend headExtra bodyExtra = do
163 --TODO: We should probably have a "NullEventWriterT" or a frozen reflex timeline
243 --TODO: We should probably have a "NullEventWriterT" or a frozen reflex timeline
164− html <- fmap snd $ renderStatic $ fmap fst $ 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) $
165 el "html" $ do
245 el "html" $ do
166− el "head" headWidget
246+ el "head" $ do
167− el "body" bodyWidget
247+ baseTag
248+ injectExecutableConfigs configs
249+ _frontend_head frontend
250+ headExtra
251+ el "body" $ do
252+ _frontend_body frontend
253+ bodyExtra
168 return $ "<!DOCTYPE html>" <> html
254 return $ "<!DOCTYPE html>" <> html
lib/frontend/src/Obelisk/Frontend/Cookie.hsnew file+103
@@ -0,0 +1,103 @@
1+{-# LANGUAGE CPP #-}
2+{-# LANGUAGE DataKinds #-}
3+{-# LANGUAGE DefaultSignatures #-}
4+{-# LANGUAGE DeriveFunctor #-}
5+{-# LANGUAGE FlexibleContexts #-}
6+{-# LANGUAGE FlexibleInstances #-}
7+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
8+{-# LANGUAGE MultiParamTypeClasses #-}
9+{-# LANGUAGE PackageImports #-}
10+{-# LANGUAGE RankNTypes #-}
11+{-# LANGUAGE TypeFamilies #-}
12+{-# LANGUAGE UndecidableInstances #-}
13+
14+module Obelisk.Frontend.Cookie where
15+
16+import Control.Monad.Fix
17+import Control.Monad.Primitive
18+import Control.Monad.Ref
19+import Control.Monad.IO.Class
20+import Control.Monad.Trans.Class
21+import Control.Monad.Trans.Reader
22+import Data.Text.Encoding (encodeUtf8)
23+import Reflex
24+import Reflex.Host.Class
25+import Reflex.Dom.Core
26+import "ghcjs-dom" GHCJS.DOM.Document (getCookie, Document)
27+import GHCJS.DOM.Types (MonadJSM)
28+import Web.Cookie
29+
30+import Obelisk.Configs
31+import Obelisk.Route.Frontend
32+
33+class Monad m => HasCookies m where
34+ askCookies :: m Cookies
35+ default askCookies :: (HasCookies m', m ~ t m', MonadTrans t) => m Cookies
36+ askCookies = lift askCookies
37+
38+instance HasCookies m => HasCookies (BehaviorWriterT t w m)
39+instance HasCookies m => HasCookies (DynamicWriterT t w m)
40+instance HasCookies m => HasCookies (EventWriterT t w m)
41+instance HasCookies m => HasCookies (PostBuildT t m)
42+instance HasCookies m => HasCookies (QueryT t q m)
43+instance HasCookies m => HasCookies (ReaderT r m)
44+instance HasCookies m => HasCookies (RequesterT t request response m)
45+instance HasCookies m => HasCookies (RouteToUrlT t m)
46+instance HasCookies m => HasCookies (SetRouteT t r m)
47+instance HasCookies m => HasCookies (StaticDomBuilderT t m)
48+instance HasCookies m => HasCookies (TriggerEventT t m)
49+instance HasCookies m => HasCookies (RoutedT t r m)
50+instance HasCookies m => HasCookies (ConfigsT m)
51+instance HasConfigs m => HasConfigs (CookiesT m)
52+
53+newtype CookiesT m a = CookiesT { unCookiesT :: ReaderT Cookies m a }
54+ deriving
55+ ( Functor
56+ , Applicative
57+ , DomBuilder t
58+ , Monad
59+ , MonadFix
60+ , MonadHold t
61+ , MonadIO
62+#ifndef ghcjs_HOST_OS
63+ , MonadJSM
64+#endif
65+ , MonadRef
66+ , MonadReflexCreateTrigger t
67+ , MonadSample t
68+ , MonadTrans
69+ , NotReady t
70+ , PerformEvent t
71+ , PostBuild t
72+ , Prerender js t
73+ , TriggerEvent t
74+ , HasDocument
75+ )
76+
77+instance Adjustable t m => Adjustable t (CookiesT m) where
78+ runWithReplace a e = CookiesT $ runWithReplace (unCookiesT a) (unCookiesT <$> e)
79+ traverseDMapWithKeyWithAdjust f m e = CookiesT $ traverseDMapWithKeyWithAdjust (\k v -> unCookiesT $ f k v) m e
80+ traverseIntMapWithKeyWithAdjust f m e = CookiesT $ traverseIntMapWithKeyWithAdjust (\k v -> unCookiesT $ f k v) m e
81+ traverseDMapWithKeyWithAdjustWithMove f m e = CookiesT $ traverseDMapWithKeyWithAdjustWithMove (\k v -> unCookiesT $ f k v) m e
82+
83+instance PrimMonad m => PrimMonad (CookiesT m) where
84+ type PrimState (CookiesT m) = PrimState m
85+ primitive = lift . primitive
86+
87+runCookiesT
88+ :: Cookies
89+ -> CookiesT m a
90+ -> m a
91+runCookiesT cs child = runReaderT (unCookiesT child) cs
92+
93+instance Monad m => HasCookies (CookiesT m) where
94+ askCookies = CookiesT ask
95+
96+mapCookiesT
97+ :: (forall x. m x -> n x)
98+ -> CookiesT m a
99+ -> CookiesT n a
100+mapCookiesT f (CookiesT x) = CookiesT $ mapReaderT f x
101+
102+instance (MonadJSM m, RawDocument (DomBuilderSpace (HydrationDomBuilderT s t m)) ~ Document) => HasCookies (HydrationDomBuilderT s t m) where
103+ askCookies = fmap (parseCookies . encodeUtf8) $ getCookie =<< askDocument
lib/route/obelisk-route.cabal+7−2
@@ -6,12 +6,14 @@ build-type: Simple
6library
6library
7 hs-source-dirs: src
7 hs-source-dirs: src
8 build-depends: base,
8 build-depends: base,
9+ aeson,
10+ bytestring,
9 categories,
11 categories,
10 constraints,
12 constraints,
13+ constraints-extras,
11 containers,
14 containers,
12 dependent-map,
15 dependent-map,
13 dependent-sum,
16 dependent-sum,
14− dependent-sum-universe-orphans,
15 dependent-sum-template,
17 dependent-sum-template,
16 either,
18 either,
17 ghcjs-dom,
19 ghcjs-dom,
@@ -21,15 +23,18 @@ library
21 monad-control,
23 monad-control,
22 mtl,
24 mtl,
23 network-uri,
25 network-uri,
26+ obelisk-executable-config-lookup,
24 primitive,
27 primitive,
25 ref-tf,
28 ref-tf,
26 reflex,
29 reflex,
27 reflex-dom-core,
30 reflex-dom-core,
28 template-haskell,
31 template-haskell,
32+ tabulation,
29 text,
33 text,
34+ th-extras,
30 transformers,
35 transformers,
31 universe,
36 universe,
32− universe-template
37+ universe-dependent-sum
33 exposed-modules: Obelisk.Route
38 exposed-modules: Obelisk.Route
34 Obelisk.Route.TH
39 Obelisk.Route.TH
35 Obelisk.Route.Frontend
40 Obelisk.Route.Frontend
lib/route/src/Obelisk/Route.hs+395−40
@@ -12,12 +12,18 @@
12{-# LANGUAGE PatternSynonyms #-}
12{-# LANGUAGE PatternSynonyms #-}
13{-# LANGUAGE RankNTypes #-}
13{-# LANGUAGE RankNTypes #-}
14{-# LANGUAGE ScopedTypeVariables #-}
14{-# LANGUAGE ScopedTypeVariables #-}
15+{-# LANGUAGE StandaloneDeriving #-}
15{-# LANGUAGE TemplateHaskell #-}
16{-# LANGUAGE TemplateHaskell #-}
17+{-# LANGUAGE TypeApplications #-}
16{-# LANGUAGE TypeFamilies #-}
18{-# LANGUAGE TypeFamilies #-}
17{-# LANGUAGE TypeOperators #-}
19{-# LANGUAGE TypeOperators #-}
18{-# LANGUAGE ViewPatterns #-}
20{-# LANGUAGE ViewPatterns #-}
19module Obelisk.Route
21module Obelisk.Route
20 ( R
22 ( R
23+ , (:.)
24+ , (?/)
25+ , hoistR
26+ , pattern (:.)
21 , pattern (:/)
27 , pattern (:/)
22 , PageName
28 , PageName
23 , PathQuery
29 , PathQuery
@@ -40,6 +46,9 @@ module Obelisk.Route
40 , checkEnum1EncoderFunc
46 , checkEnum1EncoderFunc
41 , unitEncoder
47 , unitEncoder
42 , pathOnlyEncoder
48 , pathOnlyEncoder
49+ , addPathSegmentEncoder
50+ , pathParamEncoder
51+ , pathLiteralEncoder
43 , singletonListEncoder
52 , singletonListEncoder
44 , unpackTextEncoder
53 , unpackTextEncoder
45 , prefixTextEncoder
54 , prefixTextEncoder
@@ -60,10 +69,15 @@ module Obelisk.Route
60 , shadowEncoder
69 , shadowEncoder
61 , prismEncoder
70 , prismEncoder
62 , rPrism
71 , rPrism
72+ , _R
63 , obeliskRouteEncoder
73 , obeliskRouteEncoder
64 , obeliskRouteSegment
74 , obeliskRouteSegment
65 , pageNameEncoder
75 , pageNameEncoder
66 , handleEncoder
76 , handleEncoder
77+ , FullRoute (..)
78+ , _FullRoute_Frontend
79+ , _FullRoute_Backend
80+ , mkFullRouteEncoder
67 , ObeliskRoute (..)
81 , ObeliskRoute (..)
68 , _ObeliskRoute_App
82 , _ObeliskRoute_App
69 , _ObeliskRoute_Resource
83 , _ObeliskRoute_Resource
@@ -78,6 +92,19 @@ module Obelisk.Route
78 , void1Encoder
92 , void1Encoder
79 , pathSegmentsTextEncoder
93 , pathSegmentsTextEncoder
80 , queryParametersTextEncoder
94 , queryParametersTextEncoder
95+ , renderObeliskRoute
96+ , renderBackendRoute
97+ , renderFrontendRoute
98+ , readShowEncoder
99+ , integralEncoder
100+ , pathSegmentEncoder
101+ , queryOnlyEncoder
102+ , Decoder(..)
103+ , dmapEncoder
104+ , fieldMapEncoder
105+ , pathFieldEncoder
106+ , jsonEncoder
107+ , byteStringsToPageName
81 ) where
108 ) where
82
109
83import Prelude hiding ((.), id)
110import Prelude hiding ((.), id)
@@ -88,8 +115,32 @@ import qualified Control.Categorical.Functor as Cat
88import Control.Categorical.Bifunctor
115import Control.Categorical.Bifunctor
89import Control.Category.Associative
116import Control.Category.Associative
90import Control.Category.Monoidal
117import Control.Category.Monoidal
91−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+ )
92import Control.Monad.Except
138import 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
143+import qualified Data.ByteString.Lazy as BSL
93import Data.Dependent.Sum (DSum (..))
144import Data.Dependent.Sum (DSum (..))
94import Data.Dependent.Map (DMap)
145import Data.Dependent.Map (DMap)
95import qualified Data.Dependent.Map as DMap
146import qualified Data.Dependent.Map as DMap
@@ -106,16 +157,19 @@ import Data.Maybe
106import Data.Monoid ((<>))
157import Data.Monoid ((<>))
107import Data.Set (Set)
158import Data.Set (Set)
108import qualified Data.Set as Set
159import qualified Data.Set as Set
109−import Data.Some (Some)
160+import Data.Some (Some(Some))
110−import qualified Data.Some as Some
111−import Data.Some.Universe.Orphans ()
112import Data.Text (Text)
161import Data.Text (Text)
113import qualified Data.Text as T
162import qualified Data.Text as T
114−import Data.Text.Encoding
163+import qualified Data.Text.Encoding as T
115import Data.Universe
164import Data.Universe
165+import Data.Universe.Some
116import Network.HTTP.Types.URI
166import Network.HTTP.Types.URI
167+import qualified Numeric.Lens
117import Obelisk.Route.TH
168import Obelisk.Route.TH
118import Text.Read (readMaybe)
169import Text.Read (readMaybe)
170+import Data.Tabulation
171+import qualified Data.Aeson as Aeson
172+import Data.Aeson (FromJSON, ToJSON)
119
173
120-- Design goals:
174-- Design goals:
121-- No start-up time on the frontend (not yet met)
175-- No start-up time on the frontend (not yet met)
@@ -141,13 +195,64 @@ import Text.Read (readMaybe)
141
195
142type R f = DSum f Identity --TODO: Better name
196type R f = DSum f Identity --TODO: Better name
143
197
144−--TODO: COMPLETE pragma
198+-- | Convenience builder for an 'R' using 'Identity' for the functor.
145−infixr 5 :/
146pattern (:/) :: f a -> a -> R f
199pattern (:/) :: f a -> a -> R f
147pattern a :/ b = a :=> Identity b
200pattern a :/ b = a :=> Identity b
201+{-# COMPLETE (:/) #-}
202+infixr 5 :/
203+
204+-- | Like '(:/)' but adds a 'Just' wrapper around the right-hand side.
205+(?/) :: f (Maybe a) -> a -> R f
206+r ?/ a = r :/ Just a
207+infixr 5 ?/
148
208
149mapSome :: (forall a. f a -> g a) -> Some f -> Some g
209mapSome :: (forall a. f a -> g a) -> Some f -> Some g
150−mapSome f (Some.This a) = Some.This $ f a
210+mapSome f (Some a) = Some $ f a
211+
212+hoistR :: (forall x. f x -> g x) -> R f -> R g
213+hoistR f (x :=> Identity y) = f x :/ y
214+
215+--------------------------------------------------------------------------------
216+-- Dealing with pairs (i.e. non-dependently-typed subroutes/paths)
217+--------------------------------------------------------------------------------
218+
219+infixr 5 :.
220+type (:.) = (,)
221+
222+{-# COMPLETE (:.) #-}
223+pattern (:.) :: a -> b -> a :. b
224+pattern a :. b = (a, b)
225+
226+addPathSegmentEncoder
227+ :: ( Applicative check
228+ , MonadError Text parse
229+ )
230+ => Encoder check parse (Text, PageName) PageName
231+addPathSegmentEncoder = unsafeMkEncoder $ EncoderImpl
232+ { _encoderImpl_encode = \(ph, (pt, q)) -> (ph : pt, q)
233+ , _encoderImpl_decode = \(p, q) -> case p of
234+ [] -> throwError "Expected a path segment"
235+ ph : pt -> pure (ph, (pt, q))
236+ }
237+
238+pathParamEncoder
239+ :: forall check parse item rest.
240+ ( Applicative check
241+ , MonadError Text parse
242+ )
243+ => Encoder check parse item Text
244+ -> Encoder check parse rest PageName
245+ -> Encoder check parse (item :. rest) PageName
246+pathParamEncoder itemUnchecked restUnchecked = addPathSegmentEncoder . bimap itemUnchecked restUnchecked
247+
248+pathLiteralEncoder
249+ :: ( Applicative check
250+ , MonadError Text parse
251+ )
252+ => Text
253+ -> Encoder check parse a PageName
254+ -> Encoder check parse a PageName
255+pathLiteralEncoder t e = addPathSegmentEncoder . bimap (unitEncoder t) e . coidl
151
256
152--------------------------------------------------------------------------------
257--------------------------------------------------------------------------------
153-- Encoder fundamentals
258-- Encoder fundamentals
@@ -182,7 +287,7 @@ decode e x = runIdentity (tryDecode e x)
182tryDecode :: Encoder Identity parse decoded encoded -> encoded -> parse decoded
287tryDecode :: Encoder Identity parse decoded encoded -> encoded -> parse decoded
183tryDecode (Encoder (Identity impl)) x = _encoderImpl_decode impl x
288tryDecode (Encoder (Identity impl)) x = _encoderImpl_decode impl x
184
289
185−-- | Similar to 'decode' above, once an encoder has been checked so that its check monad is Identity, it
290+-- | Similar to 'decode', once an encoder has been checked so that its check monad is Identity, it
186-- can be used to actually encode by using this. Note that while there's no constraint on the parse monad here,
291-- can be used to actually encode by using this. Note that while there's no constraint on the parse monad here,
187-- one should usually be applying decode and encode to the same 'Encoder'
292-- one should usually be applying decode and encode to the same 'Encoder'
188encode :: Encoder Identity parse decoded encoded -> decoded -> encoded
293encode :: Encoder Identity parse decoded encoded -> decoded -> encoded
@@ -232,6 +337,10 @@ instance Monad parse => Bifunctor (,) (EncoderImpl parse) (EncoderImpl parse) (E
232 , _encoderImpl_decode = \(a, b) -> liftA2 (,) (_encoderImpl_decode f a) (_encoderImpl_decode g b)
337 , _encoderImpl_decode = \(a, b) -> liftA2 (,) (_encoderImpl_decode f a) (_encoderImpl_decode g b)
233 }
338 }
234
339
340+instance (Monad parse, Applicative check) => Braided (Encoder check parse) (,) where
341+ braid = isoEncoder (iso swap swap)
342+
343+
235instance (Applicative check, Monad parse) => PFunctor (,) (Encoder check parse) (Encoder check parse) where
344instance (Applicative check, Monad parse) => PFunctor (,) (Encoder check parse) (Encoder check parse) where
236 first f = bimap f id
345 first f = bimap f id
237instance (Applicative check, Monad parse) => QFunctor (,) (Encoder check parse) (Encoder check parse) where
346instance (Applicative check, Monad parse) => QFunctor (,) (Encoder check parse) (Encoder check parse) where
@@ -245,6 +354,34 @@ instance (Traversable f, Monad parse) => Cat.Functor f (EncoderImpl parse) (Enco
245 , _encoderImpl_decode = traverse $ _encoderImpl_decode ve
354 , _encoderImpl_decode = traverse $ _encoderImpl_decode ve
246 }
355 }
247
356
357+instance Monad parse => PFunctor Either (EncoderImpl parse) (EncoderImpl parse) where
358+ first f = bimap f id
359+instance Monad parse => QFunctor Either (EncoderImpl parse) (EncoderImpl parse) where
360+ second g = bimap id g
361+instance Monad parse => Bifunctor Either (EncoderImpl parse) (EncoderImpl parse) (EncoderImpl parse) where
362+ bimap f g = EncoderImpl
363+ { _encoderImpl_encode = bimap (_encoderImpl_encode f) (_encoderImpl_encode g)
364+ , _encoderImpl_decode = \case
365+ Left a -> Left <$> _encoderImpl_decode f a
366+ Right b -> Right <$> _encoderImpl_decode g b
367+ }
368+
369+instance (Monad parse, Applicative check) => QFunctor Either (Encoder check parse) (Encoder check parse) where
370+ second g = bimap id g
371+instance (Monad parse, Applicative check) => PFunctor Either (Encoder check parse) (Encoder check parse) where
372+ first f = bimap f id
373+instance (Monad parse, Applicative check) => Bifunctor Either (Encoder check parse) (Encoder check parse) (Encoder check parse) where
374+ bimap f g = Encoder $ liftA2 bimap (unEncoder f) (unEncoder g)
375+
376+instance (Applicative check, Monad parse) => Associative (Encoder check parse) Either where
377+ associate = isoEncoder (iso (associate @(->) @Either) disassociate)
378+ disassociate = isoEncoder (iso disassociate associate)
379+
380+instance (Monad parse, Applicative check) => Braided (Encoder check parse) Either where
381+ braid = isoEncoder (iso swap swap)
382+
383+
384+
248instance (Traversable f, Monad check, Monad parse) => Cat.Functor f (Encoder check parse) (Encoder check parse) where
385instance (Traversable f, Monad check, Monad parse) => Cat.Functor f (Encoder check parse) (Encoder check parse) where
249 fmap e = Encoder $ do
386 fmap e = Encoder $ do
250 ve <- unEncoder e
387 ve <- unEncoder e
@@ -332,14 +469,14 @@ maybeEncoder f g = shadowEncoder f g . maybeToEitherEncoder
332justEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse a (Maybe a)
469justEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse a (Maybe a)
333justEncoder = prismEncoder _Just
470justEncoder = prismEncoder _Just
334
471
335−-- |
472+-- | Encode () to 'Nothing'.
336nothingEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse () (Maybe a)
473nothingEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse () (Maybe a)
337nothingEncoder = prismEncoder _Nothing
474nothingEncoder = prismEncoder _Nothing
338
475
339someConstEncoder :: (Applicative check, Applicative parse) => Encoder check parse (Some (Const a)) a
476someConstEncoder :: (Applicative check, Applicative parse) => Encoder check parse (Some (Const a)) a
340someConstEncoder = unsafeMkEncoder $ EncoderImpl
477someConstEncoder = unsafeMkEncoder $ EncoderImpl
341− { _encoderImpl_encode = \(Some.This (Const a)) -> a
478+ { _encoderImpl_encode = \(Some (Const a)) -> a
342− , _encoderImpl_decode = pure . Some.This . Const
479+ , _encoderImpl_decode = pure . Some . Const
343 }
480 }
344
481
345-- | WARNING: This is only safe if the Show and Read instances for 'a' are
482-- | WARNING: This is only safe if the Show and Read instances for 'a' are
@@ -367,7 +504,7 @@ checkEnum1EncoderFunc
367 -> check (EncoderFunc check' parse p r)
504 -> check (EncoderFunc check' parse p r)
368checkEnum1EncoderFunc f = do
505checkEnum1EncoderFunc f = do
369 (encoderImpls :: DMap p (Flip (EncoderImpl parse) r)) <- DMap.fromList <$>
506 (encoderImpls :: DMap p (Flip (EncoderImpl parse) r)) <- DMap.fromList <$>
370− traverse (\(Some.This p) -> (p :=>) . Flip <$> unEncoder (f p)) universe
507+ traverse (\(Some p) -> (p :=>) . Flip <$> unEncoder (f p)) universe
371 pure $ EncoderFunc $ \p -> unsafeMkEncoder . unFlip $
508 pure $ EncoderFunc $ \p -> unsafeMkEncoder . unFlip $
372 DMap.findWithDefault (error "checkEnum1EncoderFunc: EncoderImpl not found (should be impossible)") p encoderImpls
509 DMap.findWithDefault (error "checkEnum1EncoderFunc: EncoderImpl not found (should be impossible)") p encoderImpls
373
510
@@ -422,11 +559,12 @@ chainEncoder cons this rest = Encoder $ do
422 pure $ EncoderImpl
559 pure $ EncoderImpl
423 { _encoderImpl_decode = \v -> do
560 { _encoderImpl_decode = \v -> do
424 (here, following) <- _encoderImpl_decode consValid v
561 (here, following) <- _encoderImpl_decode consValid v
425− Some.This r <- _encoderImpl_decode thisValid here
562+ _encoderImpl_decode thisValid here >>= \case
426− (r :/) <$> _encoderImpl_decode (runIdentity . unEncoder $ rest r) following
563+ Some r ->
564+ (r :/) <$> _encoderImpl_decode (runIdentity . unEncoder $ rest r) following
427 , _encoderImpl_encode = \(r :/ s) ->
565 , _encoderImpl_encode = \(r :/ s) ->
428 _encoderImpl_encode consValid
566 _encoderImpl_encode consValid
429− ( _encoderImpl_encode thisValid $ Some.This r
567+ ( _encoderImpl_encode thisValid $ Some r
430 , _encoderImpl_encode (runIdentity . unEncoder $ rest r) s)
568 , _encoderImpl_encode (runIdentity . unEncoder $ rest r) s)
431 }
569 }
432
570
@@ -493,7 +631,7 @@ enum1Encoder
493 , Show r
631 , Show r
494 )
632 )
495 => (forall a. p a -> r) -> Encoder check parse (Some p) r
633 => (forall a. p a -> r) -> Encoder check parse (Some p) r
496−enum1Encoder f = enumEncoder $ \(Some.This p) -> f p
634+enum1Encoder f = enumEncoder $ \(Some p) -> f p
497
635
498-- | Encode an enumerable, bounded type. WARNING: Don't use this on types that
636-- | Encode an enumerable, bounded type. WARNING: Don't use this on types that
499-- have a large number of values - it will use a lot of memory.
637-- have a large number of values - it will use a lot of memory.
@@ -536,13 +674,10 @@ pathOnlyEncoderIgnoringQuery = unsafeMkEncoder $ EncoderImpl
536 }
674 }
537
675
538pathOnlyEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse [Text] PageName
676pathOnlyEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse [Text] PageName
539−pathOnlyEncoder = unsafeMkEncoder $ EncoderImpl
677+pathOnlyEncoder = second (unitEncoder mempty) . coidr
540− { _encoderImpl_decode = \(path, query) ->
678+
541− if query == mempty
679+queryOnlyEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse (Map Text (Maybe Text)) PageName
542− then pure path
680+queryOnlyEncoder = first (unitEncoder []) . coidl
543− else throwError "pathOnlyEncoderImpl: query was provided"
544− , _encoderImpl_encode = \path -> (path, mempty)
545− }
546
681
547singletonListEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse a [a]
682singletonListEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse a [a]
548singletonListEncoder = unsafeMkEncoder $ EncoderImpl
683singletonListEncoder = unsafeMkEncoder $ EncoderImpl
@@ -585,10 +720,10 @@ queryParametersTextEncoder = Encoder $ pure $ EncoderImpl
585 in (urlDecodeText True k, urlDecodeText True <$> mv)
720 in (urlDecodeText True k, urlDecodeText True <$> mv)
586
721
587urlEncodeText :: Bool -> Text -> Text
722urlEncodeText :: Bool -> Text -> Text
588−urlEncodeText q = decodeUtf8 . urlEncode q . encodeUtf8
723+urlEncodeText q = T.decodeUtf8 . urlEncode q . T.encodeUtf8
589
724
590urlDecodeText :: Bool -> Text -> Text
725urlDecodeText :: Bool -> Text -> Text
591−urlDecodeText q = decodeUtf8 . urlDecode q . encodeUtf8
726+urlDecodeText q = T.decodeUtf8 . urlDecode q . T.encodeUtf8
592
727
593listToNonEmptyEncoder :: (Applicative check, Applicative parse, Monoid a, Eq a) => Encoder check parse [a] (NonEmpty a)
728listToNonEmptyEncoder :: (Applicative check, Applicative parse, Monoid a, Eq a) => Encoder check parse [a] (NonEmpty a)
594listToNonEmptyEncoder = Encoder $ pure $ EncoderImpl
729listToNonEmptyEncoder = Encoder $ pure $ EncoderImpl
@@ -646,10 +781,47 @@ joinPairTextEncoder = Encoder . \case
646 _ -> return (kt, T.drop (T.length separator) vt)
781 _ -> return (kt, T.drop (T.length separator) vt)
647 }
782 }
648
783
649−--TODO: Rewrite this by composing the given prism with a lens on the first element of the DSum
784+-- This slight generalization of 'rPrism' happens to be enough to write all our
650−-- Or, more likely, the user can compose it themselves
785+-- prism combinators so far.
651−rPrism :: forall f g. (forall a. Prism' (f a) (g a)) -> Prism' (R f) (R g)
786+dSumPrism
652−rPrism p = prism (\(g :/ x) -> g ^. re p :/ x) (\(f :/ x) -> bimap (:/ x) (:/ x) $ matching p f)
787+ :: forall f f' g
788+ . (forall a. Prism' (f a) (f' a))
789+ -> Prism' (DSum f g) (DSum f' g)
790+dSumPrism p = prism'
791+ (\(f' :=> x) -> f' ^. re p :=> x)
792+ (\(f :=> x) -> (:=> x) <$> (f ^? p))
793+
794+-- already in obelisk
795+rPrism
796+ :: forall f f'
797+ . (forall a. Prism' (f a) (f' a))
798+ -> Prism' (R f) (R f')
799+rPrism p = dSumPrism p
800+
801+dSumPrism'
802+ :: forall f g a
803+ . (forall b. Prism' (f b) (a :~: b))
804+ -> Prism' (DSum f g) (g a)
805+dSumPrism' p = dSumPrism p . iso (\(Refl :=> b) -> b) (Refl :=>)
806+
807+dSumGEqPrism
808+ :: GEq f
809+ => f a
810+ -> Prism' (DSum f g) (g a)
811+dSumGEqPrism variant = dSumPrism' $ prism' (\Refl -> variant) (\x -> geq variant x)
812+
813+-- | Given a 'tag :: f a', make a prism for 'R f'. This generalizes the usual
814+-- prisms for a sum type (the ones that 'mkPrisms' would make), just as 'R'
815+-- generalized a usual sum type.
816+--
817+-- [This is given the '_R' name of the "cannonical" prism not because it is the
818+-- most general, but because it seems the most useful for routes, and 'R' itself
819+-- trades generality for route-specificity.]
820+_R
821+ :: GEq f
822+ => f a
823+ -> Prism' (R f) a
824+_R variant = dSumGEqPrism variant . iso runIdentity Identity
653
825
654-- | An encoder that only works on the items available via the prism. An error will be thrown in the parse monad
826-- | An encoder that only works on the items available via the prism. An error will be thrown in the parse monad
655-- if the prism doesn't match.
827-- if the prism doesn't match.
@@ -693,6 +865,45 @@ handleEncoder recover e = Encoder $ do
693-- Actual obelisk route info
865-- Actual obelisk route info
694--------------------------------------------------------------------------------
866--------------------------------------------------------------------------------
695
867
868+-- | The typical full route type comprising all of an Obelisk application's routes.
869+-- Parameterised by the top level GADTs that define backend and frontend routes, respectively.
870+data FullRoute :: (* -> *) -> (* -> *) -> * -> * where
871+ FullRoute_Backend :: br a -> FullRoute br fr a
872+ FullRoute_Frontend :: ObeliskRoute fr a -> FullRoute br fr a
873+
874+instance (GShow br, GShow fr) => GShow (FullRoute br fr) where
875+ gshowsPrec p = \case
876+ FullRoute_Backend x -> showParen (p > 10) (showString "FullRoute_Backend " . gshowsPrec 11 x)
877+ FullRoute_Frontend x -> showParen (p > 10) (showString "FullRoute_Frontend " . gshowsPrec 11 x)
878+
879+instance (GEq br, GEq fr) => GEq (FullRoute br fr) where
880+ geq (FullRoute_Backend x) (FullRoute_Backend y) = geq x y
881+ geq (FullRoute_Frontend x) (FullRoute_Frontend y) = geq x y
882+ geq _ _ = Nothing
883+
884+instance (GCompare br, GCompare fr) => GCompare (FullRoute br fr) where
885+ gcompare (FullRoute_Backend _) (FullRoute_Frontend _) = GLT
886+ gcompare (FullRoute_Frontend _) (FullRoute_Backend _) = GGT
887+ gcompare (FullRoute_Backend x) (FullRoute_Backend y) = gcompare x y
888+ gcompare (FullRoute_Frontend x) (FullRoute_Frontend y) = gcompare x y
889+
890+instance (UniverseSome br, UniverseSome fr) => UniverseSome (FullRoute br fr) where
891+ universeSome = [Some (FullRoute_Backend x) | Some x <- universeSome]
892+ ++ [Some (FullRoute_Frontend x) | Some x <- universeSome]
893+
894+-- | Build the typical top level application route encoder from a route for handling 404's,
895+-- and segment encoders for backend and frontend routes.
896+mkFullRouteEncoder
897+ :: (GCompare br, GCompare fr, GShow br, GShow fr, UniverseSome br, UniverseSome fr)
898+ => R (FullRoute br fr) -- ^ 404 handler
899+ -> (forall a. br a -> SegmentResult (Either Text) (Either Text) a) -- ^ How to encode a single backend route segment
900+ -> (forall a. fr a -> SegmentResult (Either Text) (Either Text) a) -- ^ How to encode a single frontend route segment
901+ -> Encoder (Either Text) Identity (R (FullRoute br fr)) PageName
902+mkFullRouteEncoder missing backendSegment frontendSegment = handleEncoder (const missing) $
903+ pathComponentEncoder $ \case
904+ FullRoute_Backend backendRoute -> backendSegment backendRoute
905+ FullRoute_Frontend obeliskRoute -> obeliskRouteSegment obeliskRoute frontendSegment
906+
696-- | A type which can represent Obelisk-specific resource routes, in addition to application specific routes which serve your
907-- | A type which can represent Obelisk-specific resource routes, in addition to application specific routes which serve your
697-- frontend.
908-- frontend.
698data ObeliskRoute :: (* -> *) -> * -> * where
909data ObeliskRoute :: (* -> *) -> * -> * where
@@ -700,9 +911,11 @@ data ObeliskRoute :: (* -> *) -> * -> * where
700 ObeliskRoute_App :: f a -> ObeliskRoute f a
911 ObeliskRoute_App :: f a -> ObeliskRoute f a
701 ObeliskRoute_Resource :: ResourceRoute a -> ObeliskRoute f a
912 ObeliskRoute_Resource :: ResourceRoute a -> ObeliskRoute f a
702
913
703−instance Universe (Some f) => Universe (Some (ObeliskRoute f)) where
914+instance UniverseSome f => UniverseSome (ObeliskRoute f) where
704− universe = fmap (\(Some.This x) -> Some.This (ObeliskRoute_App x)) universe
915+ universeSome = concat
705− ++ fmap (\(Some.This x) -> Some.This (ObeliskRoute_Resource x)) universe
916+ [ (\(Some x) -> Some (ObeliskRoute_App x)) <$> universe
917+ , (\(Some x) -> Some (ObeliskRoute_Resource x)) <$> universe
918+ ]
706
919
707instance GEq f => GEq (ObeliskRoute f) where
920instance GEq f => GEq (ObeliskRoute f) where
708 geq (ObeliskRoute_App x) (ObeliskRoute_App y) = geq x y
921 geq (ObeliskRoute_App x) (ObeliskRoute_App y) = geq x y
@@ -790,12 +1003,12 @@ indexOnlyRouteEncoder = pathComponentEncoder indexOnlyRouteSegment
790
1003
791someSumEncoder :: (Applicative check, Applicative parse) => Encoder check parse (Some (Sum a b)) (Either (Some a) (Some b))
1004someSumEncoder :: (Applicative check, Applicative parse) => Encoder check parse (Some (Sum a b)) (Either (Some a) (Some b))
792someSumEncoder = Encoder $ pure $ EncoderImpl
1005someSumEncoder = Encoder $ pure $ EncoderImpl
793− { _encoderImpl_encode = \(Some.This t) -> case t of
1006+ { _encoderImpl_encode = \(Some t) -> case t of
794− InL l -> Left $ Some.This l
1007+ InL l -> Left $ Some l
795− InR r -> Right $ Some.This r
1008+ InR r -> Right $ Some r
796 , _encoderImpl_decode = pure . \case
1009 , _encoderImpl_decode = pure . \case
797− Left (Some.This l) -> Some.This (InL l)
1010+ Left (Some l) -> Some (InL l)
798− Right (Some.This r) -> Some.This (InR r)
1011+ Right (Some r) -> Some (InR r)
799 }
1012 }
800
1013
801data Void1 :: * -> * where {}
1014data Void1 :: * -> * where {}
@@ -806,7 +1019,7 @@ instance Universe (Some Void1) where
806void1Encoder :: (Applicative check, MonadError Text parse) => Encoder check parse (Some Void1) a
1019void1Encoder :: (Applicative check, MonadError Text parse) => Encoder check parse (Some Void1) a
807void1Encoder = Encoder $ pure $ EncoderImpl
1020void1Encoder = Encoder $ pure $ EncoderImpl
808 { _encoderImpl_encode = \case
1021 { _encoderImpl_encode = \case
809− Some.This f -> case f of {}
1022+ Some f -> case f of {}
810 , _encoderImpl_decode = \_ -> throwError "void1Encoder: can't decode anything"
1023 , _encoderImpl_decode = \_ -> throwError "void1Encoder: can't decode anything"
811 }
1024 }
812
1025
@@ -820,8 +1033,150 @@ concat <$> mapM deriveRouteComponent
820 ]
1033 ]
821
1034
822makePrisms ''ObeliskRoute
1035makePrisms ''ObeliskRoute
823−
1036+makePrisms ''FullRoute
824deriveGEq ''Void1
1037deriveGEq ''Void1
825deriveGCompare ''Void1
1038deriveGCompare ''Void1
826
1039
1040+-- | Given a backend route and a checked route encoder, render the route (path
1041+-- and query string). See 'checkEncoder' for how to produce a checked encoder.
1042+renderBackendRoute
1043+ :: forall br a.
1044+ Encoder Identity Identity (R (FullRoute br a)) PageName
1045+ -> R br
1046+ -> Text
1047+renderBackendRoute enc = renderObeliskRoute enc . hoistR FullRoute_Backend
1048+
1049+-- | Renders a frontend route with the supplied checked encoder
1050+renderFrontendRoute
1051+ :: forall a fr.
1052+ Encoder Identity Identity (R (FullRoute a fr)) PageName
1053+ -> R fr
1054+ -> Text
1055+renderFrontendRoute enc = renderObeliskRoute enc . hoistR (FullRoute_Frontend . ObeliskRoute_App)
1056+
1057+-- | Renders a route of the form typically found in an Obelisk project
1058+renderObeliskRoute
1059+ :: forall a b.
1060+ Encoder Identity Identity (R (FullRoute a b)) PageName
1061+ -> R (FullRoute a b)
1062+ -> Text
1063+renderObeliskRoute e r =
1064+ let enc :: Encoder Identity (Either Text) (R (FullRoute a b)) PathQuery
1065+ enc = (pageNameEncoder . hoistParse (pure . runIdentity) e)
1066+ in (T.pack . uncurry (<>)) $ encode enc r
1067+
1068+readShowEncoder :: (MonadError Text parse, Read a, Show a, Applicative check) => Encoder check parse a PageName
1069+readShowEncoder = singlePathSegmentEncoder . unsafeTshowEncoder
1070+
1071+integralEncoder :: (MonadError Text parse, Applicative check, Integral a) => Encoder check parse a Integer
1072+integralEncoder = prismEncoder (Numeric.Lens.integral)
1073+
1074+pathSegmentEncoder :: (MonadError Text parse, Applicative check, Cons as as a a) =>
1075+ Encoder check parse (a, (as, b)) (as, b)
1076+pathSegmentEncoder = first (prismEncoder _Cons) . disassociate
1077+
1078+newtype Decoder check parse b a = Decoder { toEncoder :: Encoder check parse a b }
1079+
1080+dmapEncoder :: forall check parse k k' v.
1081+ ( Monad check
1082+ , MonadError Text parse
1083+ , Universe (Some k')
1084+ , Ord k
1085+ , GCompare k'
1086+ , GShow k'
1087+ )
1088+ => Encoder check parse (Some k') k
1089+ -> (forall v'. k' v' -> Encoder check parse v' v)
1090+ -> Encoder check parse (DMap k' Identity) (Map k v)
1091+dmapEncoder keyEncoder' valueEncoderFor = unsafeEncoder $ do
1092+ keyEncoder :: Encoder Identity parse (Some k') k <- checkEncoder keyEncoder'
1093+ valueDecoders :: DMap k' (Decoder Identity parse v) <- fmap DMap.fromList . forM universe $ \(Some (k' :: k' t)) -> do
1094+ ve :: Encoder Identity parse t v <- checkEncoder (valueEncoderFor k')
1095+ return $ (k' :: k' t) :=> (Decoder ve :: Decoder Identity parse v t)
1096+ let keyError k = "dmapEncoder: key `" <> k <> "' was missing from the Universe instance for its type."
1097+ return $ EncoderImpl
1098+ { _encoderImpl_encode = \dm -> Map.fromList $ do
1099+ ((k' :: k' t) :=> Identity v') <- DMap.toList dm
1100+ return ( encode keyEncoder (Some k')
1101+ , encode (toEncoder (DMap.findWithDefault (error . keyError $ gshow k') k' valueDecoders)) v'
1102+ )
1103+ , _encoderImpl_decode = \m -> fmap DMap.fromList . forM (Map.toList m) $ \(k,v) -> do
1104+ tryDecode keyEncoder k >>= \case
1105+ Some (k' :: k' t) -> case DMap.lookup k' valueDecoders of
1106+ Nothing -> throwError . T.pack . keyError $ gshow k'
1107+ Just (Decoder e) -> do
1108+ v' <- tryDecode e v
1109+ return (k' :=> Identity v')
1110+ }
1111+
1112+fieldMapEncoder :: forall check parse r.
1113+ ( Applicative check
1114+ , MonadError Text parse
1115+ , HasFields r
1116+ , Universe (Some (Field r))
1117+ , GShow (Field r)
1118+ , GCompare (Field r)
1119+ )
1120+ => Encoder check parse r (DMap (Field r) Identity)
1121+fieldMapEncoder = unsafeEncoder $ do
1122+ pure $ EncoderImpl
1123+ { _encoderImpl_encode = \r -> DMap.fromList [ f :=> Identity (indexField r f) | Some f <- universe ]
1124+ , _encoderImpl_decode = \dm -> tabulateFieldsA $ \f -> do
1125+ case DMap.lookup f dm of
1126+ Nothing -> throwError $ "fieldMapEncoder: Couldn't find key for `" <> T.pack (gshow f) <> "' in DMap."
1127+ Just (Identity v) -> return v
1128+ }
1129+
1130+-- this is in base 4.12 (GHC 8.6);
1131+newtype Ap f a = Ap {getAp :: f a}
1132+
1133+instance (Applicative f, Semigroup a) => Semigroup (Ap f a) where
1134+ Ap x <> Ap y = Ap (liftA2 (<>) x y)
1135+
1136+instance (Applicative f, Monoid a) => Monoid (Ap f a) where
1137+ mappend = (<>)
1138+ mempty = Ap (pure mempty)
1139+
1140+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]
1141+pathFieldEncoder fieldEncoder = unsafeEncoder $ do
1142+ 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)
1143+ let fieldEncoderPure :: forall x. Field a x -> Encoder Identity parse x p
1144+ fieldEncoderPure f = toEncoder (DMap.findWithDefault (error "bad") f fieldEncoderPureMap)
1145+ pure $ EncoderImpl
1146+ { _encoderImpl_encode = \(x, rest) -> execWriter $ do
1147+ _ <- traverseWithField (\f x_i -> tell (pure $ encode (fieldEncoderPure f) x_i) *> pure x_i) x
1148+ tell rest
1149+ , _encoderImpl_decode = State.runStateT $ tabulateFieldsA $ \f -> State.get >>= \case
1150+ [] -> throwError $ T.pack "not enough path components"
1151+ p:ps -> do
1152+ State.put ps
1153+ lift $ tryDecode (fieldEncoderPure f) p
1154+ }
1155+
1156+-- | Use ToJSON/FromJSON to encode to Text. The correctness of this encoder is dependent on the encoding being injective and round-tripping correctly.
1157+jsonEncoder :: forall check parse r.
1158+ ( ToJSON r
1159+ , FromJSON r
1160+ , Applicative check
1161+ , MonadError Text parse
1162+ )
1163+ => Encoder check parse r Text
1164+jsonEncoder = unsafeEncoder $ do
1165+ pure $ EncoderImpl
1166+ { _encoderImpl_encode = \r -> T.decodeUtf8 . BSL.toStrict $ Aeson.encode r
1167+ , _encoderImpl_decode = \t -> case Aeson.eitherDecodeStrict $ T.encodeUtf8 t of
1168+ Left err -> throwError ("jsonEncoder: " <> T.pack err)
1169+ Right x -> return x
1170+ }
1171+
1172+-- Useful for app server integration.
1173+-- p must not start with slashes
1174+byteStringsToPageName :: BS.ByteString -> BS.ByteString -> PageName
1175+byteStringsToPageName p q =
1176+ let pageNameEncoder' :: Encoder Identity Identity PageName (String, String)
1177+ pageNameEncoder' = bimap
1178+ (unpackTextEncoder . pathSegmentsTextEncoder . listToNonEmptyEncoder)
1179+ (unpackTextEncoder . queryParametersTextEncoder . toListMapEncoder)
1180+ in decode pageNameEncoder' (T.unpack (T.decodeUtf8 p), T.unpack (T.decodeUtf8 q))
1181+
827--TODO: decodeURIComponent as appropriate
1182--TODO: decodeURIComponent as appropriate
lib/route/src/Obelisk/Route/Frontend.hs+226−51
@@ -3,17 +3,15 @@
3{-# LANGUAGE RecursiveDo #-}
3{-# LANGUAGE RecursiveDo #-}
4{-# LANGUAGE StandaloneDeriving #-}
4{-# LANGUAGE StandaloneDeriving #-}
5{-# LANGUAGE ConstraintKinds #-}
5{-# LANGUAGE ConstraintKinds #-}
6+{-# LANGUAGE DefaultSignatures #-}
6{-# LANGUAGE FlexibleContexts #-}
7{-# LANGUAGE FlexibleContexts #-}
7{-# LANGUAGE FlexibleInstances #-}
8{-# LANGUAGE FlexibleInstances #-}
8{-# LANGUAGE GADTs #-}
9{-# LANGUAGE GADTs #-}
9−{-# LANGUAGE KindSignatures #-}
10{-# LANGUAGE LambdaCase #-}
10{-# LANGUAGE LambdaCase #-}
11−{-# LANGUAGE MultiParamTypeClasses #-}
12{-# LANGUAGE OverloadedStrings #-}
11{-# LANGUAGE OverloadedStrings #-}
13{-# LANGUAGE PatternSynonyms #-}
12{-# LANGUAGE PatternSynonyms #-}
14{-# LANGUAGE RankNTypes #-}
13{-# LANGUAGE RankNTypes #-}
15{-# LANGUAGE ScopedTypeVariables #-}
14{-# LANGUAGE ScopedTypeVariables #-}
16−{-# LANGUAGE TemplateHaskell #-}
17{-# LANGUAGE TypeFamilies #-}
15{-# LANGUAGE TypeFamilies #-}
18{-# LANGUAGE TypeOperators #-}
16{-# LANGUAGE TypeOperators #-}
19{-# LANGUAGE ViewPatterns #-}
17{-# LANGUAGE ViewPatterns #-}
@@ -33,9 +31,14 @@ module Obelisk.Route.Frontend
33 , mapRoutedT
31 , mapRoutedT
34 , subRoute
32 , subRoute
35 , subRoute_
33 , subRoute_
34+ , subPairRoute
35+ , subPairRoute_
36 , maybeRoute
36 , maybeRoute
37 , maybeRoute_
37 , maybeRoute_
38 , maybeRouted
38 , maybeRouted
39+ , eitherRoute
40+ , eitherRoute_
41+ , eitherRouted
39 , runRouteViewT
42 , runRouteViewT
40 , SetRouteT(..)
43 , SetRouteT(..)
41 , SetRoute(..)
44 , SetRoute(..)
@@ -46,6 +49,10 @@ module Obelisk.Route.Frontend
46 , runRouteToUrlT
49 , runRouteToUrlT
47 , mapRouteToUrlT
50 , mapRouteToUrlT
48 , routeLink
51 , routeLink
52+ , routeLinkDynAttr
53+ , dynRouteLink
54+ , adaptedUriPath
55+ , setAdaptedUriPath
49 ) where
56 ) where
50
57
51import Prelude hiding ((.), id)
58import Prelude hiding ((.), id)
@@ -53,23 +60,24 @@ import Prelude hiding ((.), id)
53import Obelisk.Route
60import Obelisk.Route
54
61
55import Control.Category (Category (..), (.))
62import Control.Category (Category (..), (.))
56−import Control.Category.Cartesian
63+import Control.Category.Cartesian ((&&&))
57import Control.Lens hiding (Bifunctor, bimap, universe, element)
64import Control.Lens hiding (Bifunctor, bimap, universe, element)
65+import Control.Monad ((<=<))
58import Control.Monad.Fix
66import Control.Monad.Fix
59import Control.Monad.Primitive
67import Control.Monad.Primitive
68+import Control.Monad.Reader
60import Control.Monad.Ref
69import Control.Monad.Ref
61−import Control.Monad.Trans
62import Control.Monad.Trans.Control
70import Control.Monad.Trans.Control
63−import Control.Monad.Trans.Reader
64import Data.Coerce
71import Data.Coerce
65−import Data.Constraint (Dict (..))
66import Data.Dependent.Sum (DSum (..))
72import Data.Dependent.Sum (DSum (..))
67import Data.GADT.Compare
73import Data.GADT.Compare
74+import Data.Map (Map)
68import Data.Monoid
75import Data.Monoid
69import Data.Proxy
76import Data.Proxy
70import Data.Text (Text)
77import Data.Text (Text)
71import qualified Data.Text as T
78import qualified Data.Text as T
72import Data.Functor.Compose
79import Data.Functor.Compose
80+import Data.Functor.Misc
73import Reflex.Class
81import Reflex.Class
74import Reflex.Host.Class
82import Reflex.Host.Class
75import Reflex.PostBuild.Class
83import Reflex.PostBuild.Class
@@ -82,34 +90,47 @@ import Reflex.Dom.Builder.Class
82import Data.Type.Coercion
90import Data.Type.Coercion
83import Language.Javascript.JSaddle --TODO: Get rid of this - other platforms can also be routed
91import Language.Javascript.JSaddle --TODO: Get rid of this - other platforms can also be routed
84import Reflex.Dom.Core
92import Reflex.Dom.Core
93+import qualified GHCJS.DOM as DOM
85import qualified GHCJS.DOM.Types as DOM
94import qualified GHCJS.DOM.Types as DOM
95+import qualified GHCJS.DOM.Window as Window
86import Network.URI
96import Network.URI
87−#if defined(ios_HOST_OS)
88import Data.Maybe (fromMaybe)
97import Data.Maybe (fromMaybe)
89import qualified Data.List as L
98import qualified Data.List as L
90−#endif
91
99
92import Unsafe.Coerce
100import Unsafe.Coerce
93
101
102+import Obelisk.Configs
103+
94infixr 5 :~
104infixr 5 :~
95pattern (:~) :: Reflex t => f a -> Dynamic t a -> DSum f (Compose (Dynamic t) Identity)
105pattern (:~) :: Reflex t => f a -> Dynamic t a -> DSum f (Compose (Dynamic t) Identity)
96pattern a :~ b <- a :=> (coerceDynamic . getCompose -> b)
106pattern a :~ b <- a :=> (coerceDynamic . getCompose -> b)
97
107
98class Routed t r m | m -> t r where
108class Routed t r m | m -> t r where
99 askRoute :: m (Dynamic t r)
109 askRoute :: m (Dynamic t r)
110+ default askRoute :: (Monad m', MonadTrans f, Routed t r m', m ~ f m') => m (Dynamic t r)
111+ askRoute = lift askRoute
100
112
101instance Monad m => Routed t r (RoutedT t r m) where
113instance Monad m => Routed t r (RoutedT t r m) where
102 askRoute = RoutedT ask
114 askRoute = RoutedT ask
103
115
116+instance (Monad m, Routed t r m) => Routed t r (ReaderT r' m)
117+
104newtype RoutedT t r m a = RoutedT { unRoutedT :: ReaderT (Dynamic t r) m a }
118newtype RoutedT t r m a = RoutedT { unRoutedT :: ReaderT (Dynamic t r) m a }
105− deriving (Functor, Applicative, Monad, MonadFix, MonadTrans, NotReady t, MonadHold t, MonadSample t, PostBuild t, TriggerEvent t, MonadIO, MonadReflexCreateTrigger t, HasDocument)
119+ deriving (Functor, Applicative, Monad, MonadFix, MonadTrans, NotReady t, MonadHold t, MonadSample t, PostBuild t, TriggerEvent t, MonadIO, MonadReflexCreateTrigger t, HasDocument, DomRenderHook t)
120+
121+instance MonadReader r' m => MonadReader r' (RoutedT t r m) where
122+ ask = lift ask
123+ local = mapRoutedT . local
106
124
107instance HasJSContext m => HasJSContext (RoutedT t r m) where
125instance HasJSContext m => HasJSContext (RoutedT t r m) where
108 type JSContextPhantom (RoutedT t r m) = JSContextPhantom m
126 type JSContextPhantom (RoutedT t r m) = JSContextPhantom m
109 askJSContext = lift askJSContext
127 askJSContext = lift askJSContext
110
128
111−instance Prerender js m => Prerender js (RoutedT t r m) where
129+instance (Prerender js t m, Monad m) => Prerender js t (RoutedT t r m) where
112− prerenderClientDict = fmap (\Dict -> Dict) (prerenderClientDict :: Maybe (Dict (PrerenderClientConstraint js m)))
130+ type Client (RoutedT t r m) = RoutedT t r (Client m)
131+ prerender server client = RoutedT $ do
132+ r <- ask
133+ lift $ prerender (runRoutedT server r) (runRoutedT client r)
113
134
114instance Requester t m => Requester t (RoutedT t r m) where
135instance Requester t m => Requester t (RoutedT t r m) where
115 type Request (RoutedT t r m) = Request m
136 type Request (RoutedT t r m) = Request m
@@ -161,17 +182,20 @@ instance (Monad m, MonadQuery t vs m) => MonadQuery t vs (RoutedT t r m) where
161 askQueryResult = lift askQueryResult
182 askQueryResult = lift askQueryResult
162 queryIncremental = lift . queryIncremental
183 queryIncremental = lift . queryIncremental
163
184
164−instance (Monad m, RouteToUrl r m) => RouteToUrl r (QueryT t q m) where
185+instance HasConfigs m => HasConfigs (RoutedT t r m)
165− askRouteToUrl = lift askRouteToUrl
166
186
167−instance (Monad m, SetRoute t r m) => SetRoute t r (QueryT t q m) where
187+instance (Monad m, RouteToUrl r m) => RouteToUrl r (QueryT t q m)
168− setRoute = lift . setRoute
188+
169− modifyRoute = lift . modifyRoute
189+instance (Monad m, SetRoute t r m) => SetRoute t r (QueryT t q m)
190+
191+instance (Monad m, RouteToUrl r m) => RouteToUrl r (EventWriterT t w m)
192+
193+instance (Monad m, SetRoute t r m) => SetRoute t r (EventWriterT t w m)
170
194
171runRoutedT :: RoutedT t r m a -> Dynamic t r -> m a
195runRoutedT :: RoutedT t r m a -> Dynamic t r -> m a
172runRoutedT = runReaderT . unRoutedT
196runRoutedT = runReaderT . unRoutedT
173
197
174−mapRoutedT :: (m a -> n a) -> RoutedT t r m a -> RoutedT t r n a
198+mapRoutedT :: (m a -> n b) -> RoutedT t r m a -> RoutedT t r n b
175mapRoutedT f = RoutedT . mapReaderT f . unRoutedT
199mapRoutedT f = RoutedT . mapReaderT f . unRoutedT
176
200
177withRoutedT :: (Dynamic t r -> Dynamic t r') -> RoutedT t r' m a -> RoutedT t r m a
201withRoutedT :: (Dynamic t r -> Dynamic t r') -> RoutedT t r' m a -> RoutedT t r m a
@@ -181,10 +205,18 @@ subRoute_ :: (MonadFix m, MonadHold t m, GEq r, Adjustable t m) => (forall a. r
181subRoute_ f = factorRouted $ strictDynWidget_ $ \(c :=> r') -> do
205subRoute_ f = factorRouted $ strictDynWidget_ $ \(c :=> r') -> do
182 runRoutedT (f c) r'
206 runRoutedT (f c) r'
183
207
208+-- | Like 'subRoute_', but with a pair rather than an R
209+subPairRoute_ :: (MonadFix m, MonadHold t m, Eq a, Adjustable t m) => (a -> RoutedT t b m ()) -> RoutedT t (a, b) m ()
210+subPairRoute_ f = withRoutedT (fmap (\(a, b) -> Const2 a :/ b)) $ subRoute_ (\(Const2 a) -> f a)
211+
184subRoute :: (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)
212subRoute :: (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)
185subRoute f = factorRouted $ strictDynWidget $ \(c :=> r') -> do
213subRoute f = factorRouted $ strictDynWidget $ \(c :=> r') -> do
186 runRoutedT (f c) r'
214 runRoutedT (f c) r'
187
215
216+-- | Like 'subRoute_', but with a pair rather than an R
217+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)
218+subPairRoute f = withRoutedT (fmap (\(a, b) -> Const2 a :/ b)) $ subRoute (\(Const2 a) -> f a)
219+
188maybeRoute_ :: (MonadFix m, MonadHold t m, Adjustable t m) => m () -> RoutedT t r m () -> RoutedT t (Maybe r) m ()
220maybeRoute_ :: (MonadFix m, MonadHold t m, Adjustable t m) => m () -> RoutedT t r m () -> RoutedT t (Maybe r) m ()
189maybeRoute_ n j = maybeRouted $ strictDynWidget_ $ \case
221maybeRoute_ n j = maybeRouted $ strictDynWidget_ $ \case
190 Nothing -> n
222 Nothing -> n
@@ -201,6 +233,20 @@ maybeRoute f = factorRouted $ strictDynWidget $ \(c :=> r') -> do
201 runRoutedT (f c) r'
233 runRoutedT (f c) r'
202-}
234-}
203
235
236+eitherRoute_
237+ :: (MonadFix m, MonadHold t m, Adjustable t m)
238+ => RoutedT t l m ()
239+ -> RoutedT t r m ()
240+ -> RoutedT t (Either l r) m ()
241+eitherRoute_ l r = eitherRouted $ strictDynWidget_ $ either (runRoutedT l) (runRoutedT r)
242+
243+eitherRoute
244+ :: (MonadFix m, MonadHold t m, Adjustable t m)
245+ => RoutedT t l m a
246+ -> RoutedT t r m a
247+ -> RoutedT t (Either l r) m (Dynamic t a)
248+eitherRoute l r = eitherRouted $ strictDynWidget $ either (runRoutedT l) (runRoutedT r)
249+
204dsumValueCoercion :: Coercion f g -> Coercion (DSum k f) (DSum k g)
250dsumValueCoercion :: Coercion f g -> Coercion (DSum k f) (DSum k g)
205dsumValueCoercion Coercion = Coercion
251dsumValueCoercion Coercion = Coercion
206
252
@@ -217,6 +263,9 @@ maybeRouted r = RoutedT $ ReaderT $ \d -> do
217 d' <- maybeDyn d
263 d' <- maybeDyn d
218 runRoutedT r d'
264 runRoutedT r d'
219
265
266+eitherRouted :: (Reflex t, MonadFix m, MonadHold t m) => RoutedT t (Either (Dynamic t a) (Dynamic t b)) m c -> RoutedT t (Either a b) m c
267+eitherRouted r = RoutedT $ ReaderT $ runRoutedT r <=< eitherDyn
268+
220-- | WARNING: The input 'Dynamic' must be fully constructed when this is run
269-- | WARNING: The input 'Dynamic' must be fully constructed when this is run
221strictDynWidget :: (MonadSample t m, MonadHold t m, Adjustable t m) => (a -> m b) -> RoutedT t a m (Dynamic t b)
270strictDynWidget :: (MonadSample t m, MonadHold t m, Adjustable t m) => (a -> m b) -> RoutedT t a m (Dynamic t b)
222strictDynWidget f = RoutedT $ ReaderT $ \r -> do
271strictDynWidget f = RoutedT $ ReaderT $ \r -> do
@@ -231,7 +280,7 @@ strictDynWidget_ f = RoutedT $ ReaderT $ \r -> do
231 pure ()
280 pure ()
232
281
233newtype SetRouteT t r m a = SetRouteT { unSetRouteT :: EventWriterT t (Endo r) m a }
282newtype SetRouteT t r m a = SetRouteT { unSetRouteT :: EventWriterT t (Endo r) m a }
234− deriving (Functor, Applicative, Monad, MonadFix, MonadTrans, MonadIO, NotReady t, MonadHold t, MonadSample t, PostBuild t, TriggerEvent t, MonadReflexCreateTrigger t, HasDocument)
283+ deriving (Functor, Applicative, Monad, MonadFix, MonadTrans, MonadIO, NotReady t, MonadHold t, MonadSample t, PostBuild t, TriggerEvent t, MonadReflexCreateTrigger t, HasDocument, DomRenderHook t)
235
284
236instance (MonadFix m, MonadHold t m, DomBuilder t m) => DomBuilder t (SetRouteT t r m) where
285instance (MonadFix m, MonadHold t m, DomBuilder t m) => DomBuilder t (SetRouteT t r m) where
237 type DomBuilderSpace (SetRouteT t r m) = DomBuilderSpace m
286 type DomBuilderSpace (SetRouteT t r m) = DomBuilderSpace m
@@ -253,16 +302,26 @@ runSetRouteT = runEventWriterT . unSetRouteT
253class Reflex t => SetRoute t r m | m -> t r where
302class Reflex t => SetRoute t r m | m -> t r where
254 setRoute :: Event t r -> m ()
303 setRoute :: Event t r -> m ()
255 modifyRoute :: Event t (r -> r) -> m ()
304 modifyRoute :: Event t (r -> r) -> m ()
305+ default modifyRoute :: (Monad m', MonadTrans f, SetRoute t r m', m ~ f m') => Event t (r -> r) -> m ()
306+ modifyRoute = lift . modifyRoute
307+
256 setRoute = modifyRoute . fmap const
308 setRoute = modifyRoute . fmap const
257
309
258instance (Reflex t, Monad m) => SetRoute t r (SetRouteT t r m) where
310instance (Reflex t, Monad m) => SetRoute t r (SetRouteT t r m) where
259 modifyRoute = SetRouteT . tellEvent . fmap Endo
311 modifyRoute = SetRouteT . tellEvent . fmap Endo
260
312
261−instance (Monad m, SetRoute t r m) => SetRoute t r (RoutedT t r' m) where
313+instance (Monad m, SetRoute t r m) => SetRoute t r (RoutedT t r' m)
262− modifyRoute = lift . modifyRoute
263
314
264−instance Prerender js m => Prerender js (SetRouteT t r m) where
315+instance (Monad m, SetRoute t r m) => SetRoute t r (ReaderT r' m)
265− prerenderClientDict = fmap (\Dict -> Dict) (prerenderClientDict :: Maybe (Dict (PrerenderClientConstraint js m)))
316+
317+instance (PerformEvent t m, Prerender js t m, Monad m, Reflex t) => Prerender js t (SetRouteT t r m) where
318+ type Client (SetRouteT t r m) = SetRouteT t r (Client m)
319+ prerender server client = do
320+ d <- lift $ prerender (runSetRouteT server) (runSetRouteT client)
321+ let (a, r) = splitDynPure d
322+ -- Must be prompt here
323+ SetRouteT . tellEvent $ switchPromptlyDyn r
324+ pure a
266
325
267instance Requester t m => Requester t (SetRouteT t r m) where
326instance Requester t m => Requester t (SetRouteT t r m) where
268 type Request (SetRouteT t r m) = Request m
327 type Request (SetRouteT t r m) = Request m
@@ -270,6 +329,8 @@ instance Requester t m => Requester t (SetRouteT t r m) where
270 requesting = SetRouteT . requesting
329 requesting = SetRouteT . requesting
271 requesting_ = SetRouteT . requesting_
330 requesting_ = SetRouteT . requesting_
272
331
332+instance (Monad m, SetRoute t r m) => SetRoute t r (RequesterT t req rsp m)
333+
273#ifndef ghcjs_HOST_OS
334#ifndef ghcjs_HOST_OS
274deriving instance MonadJSM m => MonadJSM (SetRouteT t r m)
335deriving instance MonadJSM m => MonadJSM (SetRouteT t r m)
275#endif
336#endif
@@ -293,6 +354,8 @@ instance PrimMonad m => PrimMonad (SetRouteT t r m ) where
293 type PrimState (SetRouteT t r m) = PrimState m
354 type PrimState (SetRouteT t r m) = PrimState m
294 primitive = lift . primitive
355 primitive = lift . primitive
295
356
357+instance HasConfigs m => HasConfigs (SetRouteT t r m)
358+
296instance (MonadHold t m, Adjustable t m) => Adjustable t (SetRouteT t r m) where
359instance (MonadHold t m, Adjustable t m) => Adjustable t (SetRouteT t r m) where
297 runWithReplace a0 a' = SetRouteT $ runWithReplace (coerce a0) $ coerceEvent a'
360 runWithReplace a0 a' = SetRouteT $ runWithReplace (coerce a0) $ coerceEvent a'
298 traverseIntMapWithKeyWithAdjust f a0 a' = SetRouteT $ traverseIntMapWithKeyWithAdjust (coerce f) (coerce a0) $ coerce a'
361 traverseIntMapWithKeyWithAdjust f a0 a' = SetRouteT $ traverseIntMapWithKeyWithAdjust (coerce f) (coerce a0) $ coerce a'
@@ -306,9 +369,11 @@ instance (Monad m, MonadQuery t vs m) => MonadQuery t vs (SetRouteT t r m) where
306
369
307class RouteToUrl r m | m -> r where
370class RouteToUrl r m | m -> r where
308 askRouteToUrl :: m (r -> Text)
371 askRouteToUrl :: m (r -> Text)
372+ default askRouteToUrl :: (Monad m', MonadTrans f, RouteToUrl r m', m ~ f m') => m (r -> Text)
373+ askRouteToUrl = lift askRouteToUrl
309
374
310newtype RouteToUrlT r m a = RouteToUrlT { unRouteToUrlT :: ReaderT (r -> Text) m a }
375newtype RouteToUrlT r m a = RouteToUrlT { unRouteToUrlT :: ReaderT (r -> Text) m a }
311− deriving (Functor, Applicative, Monad, MonadFix, MonadTrans, NotReady t, MonadHold t, MonadSample t, PostBuild t, TriggerEvent t, MonadIO, MonadReflexCreateTrigger t, HasDocument)
376+ deriving (Functor, Applicative, Monad, MonadFix, MonadTrans, NotReady t, MonadHold t, MonadSample t, PostBuild t, TriggerEvent t, MonadIO, MonadReflexCreateTrigger t, HasDocument, DomRenderHook t)
312
377
313runRouteToUrlT
378runRouteToUrlT
314 :: RouteToUrlT r m a
379 :: RouteToUrlT r m a
@@ -323,17 +388,22 @@ instance Monad m => RouteToUrl r (RouteToUrlT r m) where
323 askRouteToUrl = RouteToUrlT ask
388 askRouteToUrl = RouteToUrlT ask
324
389
325instance (Monad m, RouteToUrl r m) => RouteToUrl r (SetRouteT t r' m) where
390instance (Monad m, RouteToUrl r m) => RouteToUrl r (SetRouteT t r' m) where
326− askRouteToUrl = lift askRouteToUrl
327
391
328instance (Monad m, RouteToUrl r m) => RouteToUrl r (RoutedT t r' m) where
392instance (Monad m, RouteToUrl r m) => RouteToUrl r (RoutedT t r' m) where
329− askRouteToUrl = lift askRouteToUrl
393+
394+instance (Monad m, RouteToUrl r m) => RouteToUrl r (ReaderT r' m) where
395+
396+instance (Monad m, RouteToUrl r m) => RouteToUrl r (RequesterT t req rsp m)
330
397
331instance HasJSContext m => HasJSContext (RouteToUrlT r m) where
398instance HasJSContext m => HasJSContext (RouteToUrlT r m) where
332 type JSContextPhantom (RouteToUrlT r m) = JSContextPhantom m
399 type JSContextPhantom (RouteToUrlT r m) = JSContextPhantom m
333 askJSContext = lift askJSContext
400 askJSContext = lift askJSContext
334
401
335−instance Prerender js m => Prerender js (RouteToUrlT r m) where
402+instance (Prerender js t m, Monad m) => Prerender js t (RouteToUrlT r m) where
336− prerenderClientDict = fmap (\Dict -> Dict) (prerenderClientDict :: Maybe (Dict (PrerenderClientConstraint js m)))
403+ type Client (RouteToUrlT r m) = RouteToUrlT r (Client m)
404+ prerender server client = do
405+ r <- RouteToUrlT ask
406+ lift $ prerender (runRouteToUrlT server r) (runRouteToUrlT client r)
337
407
338instance Requester t m => Requester t (RouteToUrlT r m) where
408instance Requester t m => Requester t (RouteToUrlT r m) where
339 type Request (RouteToUrlT r m) = Request m
409 type Request (RouteToUrlT r m) = Request m
@@ -383,6 +453,8 @@ instance (Monad m, MonadQuery t vs m) => MonadQuery t vs (RouteToUrlT r m) where
383 askQueryResult = lift askQueryResult
453 askQueryResult = lift askQueryResult
384 queryIncremental = lift . queryIncremental
454 queryIncremental = lift . queryIncremental
385
455
456+instance HasConfigs m => HasConfigs (RouteToUrlT t m)
457+
386runRouteViewT
458runRouteViewT
387 :: forall t m r a.
459 :: forall t m r a.
388 ( TriggerEvent t m
460 ( TriggerEvent t m
@@ -393,16 +465,20 @@ runRouteViewT
393 , MonadFix m
465 , MonadFix m
394 )
466 )
395 => (Encoder Identity Identity r PageName)
467 => (Encoder Identity Identity r PageName)
468+ --TODO: Get rid of the switchover and useHash arguments
469+ -- useHash can probably be baked into the encoder
470+ -> Event t () -- ^ Switchover event, nothing is done until this event fires. Used to prevent incorrect DOM expectations at hydration switchover time
471+ -> Bool
396 -> RoutedT t r (SetRouteT t r (RouteToUrlT r m)) a
472 -> RoutedT t r (SetRouteT t r (RouteToUrlT r m)) a
397 -> m a
473 -> m a
398−runRouteViewT routeEncoder a = do
474+runRouteViewT routeEncoder switchover useHash a = do
399− rec historyState <- manageHistory $ HistoryCommand_PushState <$> setState
475+ rec historyState <- manageHistory' switchover $ HistoryCommand_PushState <$> setState
400 let theEncoder = pageNameEncoder . hoistParse (pure . runIdentity) routeEncoder
476 let theEncoder = pageNameEncoder . hoistParse (pure . runIdentity) routeEncoder
401 -- NB: The can only fail if the uriPath doesn't begin with a '/' or if the uriQuery
477 -- NB: The can only fail if the uriPath doesn't begin with a '/' or if the uriQuery
402 -- is nonempty, but begins with a character that isn't '?'. Since we don't expect
478 -- is nonempty, but begins with a character that isn't '?'. Since we don't expect
403 -- this ever to happen, we'll just handle it by failing completely with 'error'.
479 -- this ever to happen, we'll just handle it by failing completely with 'error'.
404 route :: Dynamic t r
480 route :: Dynamic t r
405− route = fmap (errorLeft . tryDecode theEncoder . (adaptedUriPath &&& uriQuery) . _historyItem_uri) historyState
481+ route = fmap (errorLeft . tryDecode theEncoder . (adaptedUriPath useHash &&& uriQuery) . _historyItem_uri) historyState
406 where
482 where
407 errorLeft (Left e) = error (T.unpack e)
483 errorLeft (Left e) = error (T.unpack e)
408 errorLeft (Right x) = x
484 errorLeft (Right x) = x
@@ -423,7 +499,7 @@ runRouteViewT routeEncoder a = do
423 -- we can change this function later to accommodate.
499 -- we can change this function later to accommodate.
424 -- See: https://github.com/whatwg/html/issues/2174
500 -- See: https://github.com/whatwg/html/issues/2174
425 , _historyStateUpdate_title = ""
501 , _historyStateUpdate_title = ""
426− , _historyStateUpdate_uri = Just $ setAdaptedUriPath newPath $ (_historyItem_uri currentHistoryState)
502+ , _historyStateUpdate_uri = Just $ setAdaptedUriPath useHash newPath $ (_historyItem_uri currentHistoryState)
427 { uriQuery = newQuery
503 { uriQuery = newQuery
428 }
504 }
429 }
505 }
@@ -433,42 +509,141 @@ runRouteViewT routeEncoder a = do
433-- | A link widget that, when clicked, sets the route to the provided route. In non-javascript
509-- | A link widget that, when clicked, sets the route to the provided route. In non-javascript
434-- contexts, this widget falls back to using @href@s to control navigation
510-- contexts, this widget falls back to using @href@s to control navigation
435routeLink
511routeLink
436− :: forall t m a route.
512+ :: forall t m a route js.
437 ( DomBuilder t m
513 ( DomBuilder t m
438− , RouteToUrl (R route) m
514+ , RouteToUrl route m
439− , SetRoute t (R route) m
515+ , SetRoute t route m
516+ , Prerender js t m
440 )
517 )
441− => R route -- ^ Target route
518+ => route -- ^ Target route
442 -> m a -- ^ Child widget
519 -> m a -- ^ Child widget
443 -> m a
520 -> m a
444routeLink r w = do
521routeLink r w = do
522+ (e, a) <- routeLinkImpl r w
523+ scrollToTop e
524+ return a
525+
526+-- | Raw implementation of 'routeLink'. Does not scroll to the top of the page on clicks.
527+routeLinkImpl
528+ :: forall t m a route.
529+ ( DomBuilder t m
530+ , RouteToUrl route m
531+ , SetRoute t route m
532+ )
533+ => route -- ^ Target route
534+ -> m a -- ^ Child widget
535+ -> m (Event t (), a)
536+routeLinkImpl r w = do
445 enc <- askRouteToUrl
537 enc <- askRouteToUrl
446 let cfg = (def :: ElementConfig EventResult t (DomBuilderSpace m))
538 let cfg = (def :: ElementConfig EventResult t (DomBuilderSpace m))
447 & elementConfig_eventSpec %~ addEventSpecFlags (Proxy :: Proxy (DomBuilderSpace m)) Click (\_ -> preventDefault)
539 & elementConfig_eventSpec %~ addEventSpecFlags (Proxy :: Proxy (DomBuilderSpace m)) Click (\_ -> preventDefault)
448 & elementConfig_initialAttributes .~ "href" =: enc r
540 & elementConfig_initialAttributes .~ "href" =: enc r
449 (e, a) <- element "a" cfg w
541 (e, a) <- element "a" cfg w
450 setRoute $ r <$ domEvent Click e
542 setRoute $ r <$ domEvent Click e
543+ return (domEvent Click e, a)
544+
545+scrollToTop :: forall m t js. (Prerender js t m, Monad m) => Event t () -> m ()
546+scrollToTop e = prerender_ blank $ performEvent_ $ ffor e $ \_ -> liftJSM $ DOM.currentWindow >>= \case
547+ Nothing -> pure ()
548+ Just win -> Window.scrollTo win 0 0
549+
550+-- | Like 'routeLinkDynAttr' but without custom attributes.
551+dynRouteLink
552+ :: forall t m a route js.
553+ ( DomBuilder t m
554+ , PostBuild t m
555+ , RouteToUrl route m
556+ , SetRoute t route m
557+ , Prerender js t m
558+ )
559+ => Dynamic t route -- ^ Target route
560+ -> m a -- ^ Child widget
561+ -> m a
562+dynRouteLink r w = do
563+ (e, a) <- dynRouteLinkImpl r w
564+ scrollToTop e
565+ return a
566+
567+-- | Raw implementation of 'dynRouteLink'. Does not scroll to the top of the page on clicks.
568+dynRouteLinkImpl
569+ :: forall t m a route.
570+ ( DomBuilder t m
571+ , PostBuild t m
572+ , RouteToUrl route m
573+ , SetRoute t route m
574+ )
575+ => Dynamic t route -- ^ Target route
576+ -> m a -- ^ Child widget
577+ -> m (Event t (), a)
578+dynRouteLinkImpl dr w = do
579+ enc <- askRouteToUrl
580+ er <- dynamicAttributesToModifyAttributes $ ("href" =:) . enc <$> dr
581+ let cfg = (def :: ElementConfig EventResult t (DomBuilderSpace m))
582+ & elementConfig_eventSpec %~ addEventSpecFlags (Proxy :: Proxy (DomBuilderSpace m)) Click (\_ -> preventDefault)
583+ & elementConfig_modifyAttributes .~ er
584+ (e, a) <- element "a" cfg w
585+ let clk = domEvent Click e
586+ setRoute $ tag (current dr) clk
587+ return (clk, a)
588+
589+-- | An @a@-tag link widget that, when clicked, sets the route to current value of the
590+-- provided dynamic route. In non-JavaScript contexts the value of the dynamic post
591+-- build is used so the link still works like 'routeLink'.
592+routeLinkDynAttr
593+ :: forall t m a route js.
594+ ( DomBuilder t m
595+ , PostBuild t m
596+ , RouteToUrl (R route) m
597+ , SetRoute t (R route) m
598+ , Prerender js t m
599+ )
600+ => Dynamic t (Map AttributeName Text) -- ^ Attributes for @a@ element. Note that if @href@ is present it will be ignored
601+ -> Dynamic t (R route) -- ^ Target route
602+ -> m a -- ^ Child widget of the @a@ element
603+ -> m a
604+routeLinkDynAttr dAttr dr w = do
605+ (e, a) <- routeLinkDynAttrImpl dAttr dr w
606+ scrollToTop e
451 return a
607 return a
452
608
609+-- | Raw implementation of 'routeLinkDynAttr'. Does not scroll to the top of the page on clicks.
610+routeLinkDynAttrImpl
611+ :: forall t m a route.
612+ ( DomBuilder t m
613+ , PostBuild t m
614+ , RouteToUrl (R route) m
615+ , SetRoute t (R route) m
616+ )
617+ => Dynamic t (Map AttributeName Text) -- ^ Attributes for @a@ element. Note that if @href@ is present it will be ignored
618+ -> Dynamic t (R route) -- ^ Target route
619+ -> m a -- ^ Child widget of the @a@ element
620+ -> m (Event t (), a)
621+routeLinkDynAttrImpl dAttr dr w = do
622+ enc <- askRouteToUrl
623+ er <- dynamicAttributesToModifyAttributes $ zipDynWith (<>) (("href" =:) . enc <$> dr) dAttr
624+ let cfg = (def :: ElementConfig EventResult t (DomBuilderSpace m))
625+ & elementConfig_eventSpec %~ addEventSpecFlags (Proxy :: Proxy (DomBuilderSpace m)) Click (\_ -> preventDefault)
626+ & elementConfig_modifyAttributes .~ er
627+ (e, a) <- element "a" cfg w
628+ let clk = domEvent Click e
629+ setRoute $ tag (current dr) clk
630+ return (clk, a)
631+
453-- On ios due to sandboxing when loading the page from a file adapt the
632-- On ios due to sandboxing when loading the page from a file adapt the
454-- path to be based on the hash.
633-- path to be based on the hash.
455
634
456−adaptedUriPath :: URI -> String
635+adaptedUriPath :: Bool -> URI -> String
457−#if defined(ios_HOST_OS)
636+adaptedUriPath = \case
458−adaptedUriPath = hashToPath . uriFragment
637+ True -> hashToPath . uriFragment
459−
638+ False -> uriPath
460−hashToPath :: String -> String
461−hashToPath = ('/' :) . fromMaybe "" . L.stripPrefix "#"
462−#else
463−adaptedUriPath = uriPath
464−#endif
465
639
466−setAdaptedUriPath :: String -> URI -> URI
640+setAdaptedUriPath :: Bool -> String -> URI -> URI
467−#if defined(ios_HOST_OS)
641+setAdaptedUriPath useHash s u = case useHash of
468−setAdaptedUriPath s u = u { uriFragment = pathToHash s }
642+ True -> u { uriFragment = pathToHash s }
643+ False -> u { uriPath = s }
469
644
470pathToHash :: String -> String
645pathToHash :: String -> String
471pathToHash = ('#' :) . fromMaybe "" . L.stripPrefix "/"
646pathToHash = ('#' :) . fromMaybe "" . L.stripPrefix "/"
472−#else
647+
473−setAdaptedUriPath s u = u { uriPath = s }
648+hashToPath :: String -> String
474−#endif
649+hashToPath = ('/' :) . fromMaybe "" . L.stripPrefix "#"
lib/route/src/Obelisk/Route/TH.hs+4−5
@@ -1,8 +1,9 @@
1module Obelisk.Route.TH (deriveRouteComponent) where
1module Obelisk.Route.TH (deriveRouteComponent) where
2
2
3+import Data.Constraint.Extras.TH
3import Data.GADT.Show.TH
4import Data.GADT.Show.TH
4import Data.GADT.Compare.TH
5import Data.GADT.Compare.TH
5−import Data.Universe.TH
6+import Data.Universe.Some.TH
6import Language.Haskell.TH
7import Language.Haskell.TH
7
8
8-- | Derive all the typeclasses needed for a RouteComponent type. The argument should be the name of a type of kind @k -> *@
9-- | Derive all the typeclasses needed for a RouteComponent type. The argument should be the name of a type of kind @k -> *@
@@ -11,8 +12,6 @@ deriveRouteComponent x = concat <$> traverse ($ x)
11 [ deriveGShow
12 [ deriveGShow
12 , deriveGEq
13 , deriveGEq
13 , deriveGCompare
14 , deriveGCompare
14− , deriveShowTagIdentity
15+ , deriveUniverseSome
15− , deriveEqTagIdentity
16+ , deriveArgDict
16− , deriveOrdTagIdentity
17− , deriveSomeUniverse
18 ]
17 ]
lib/run/obelisk-run.cabal+10−2
@@ -10,9 +10,12 @@ library
10 attoparsec
10 attoparsec
11 , base
11 , base
12 , bytestring
12 , bytestring
13+ , containers
14+ , cookie
13 , dependent-sum
15 , dependent-sum
14 , dependent-sum-template
16 , dependent-sum-template
15 , ghcjs-dom
17 , ghcjs-dom
18+ , HsOpenSSL
16 , http-client
19 , http-client
17 , http-reverse-proxy
20 , http-reverse-proxy
18 , http-types
21 , http-types
@@ -24,8 +27,7 @@ library
24 , network
27 , network
25 , obelisk-asset-serve-snap
28 , obelisk-asset-serve-snap
26 , obelisk-backend
29 , obelisk-backend
27− , obelisk-executable-config
30+ , obelisk-executable-config-lookup
28− , obelisk-executable-config-inject
29 , obelisk-frontend
31 , obelisk-frontend
30 , obelisk-route
32 , obelisk-route
31 , process
33 , process
@@ -35,11 +37,17 @@ library
35 , snap-core
37 , snap-core
36 , streaming-commons
38 , streaming-commons
37 , text
39 , text
40+ , time
38 , universe
41 , universe
42+ , utf8-string
39 , wai
43 , wai
40 , wai-websockets
44 , wai-websockets
41 , warp
45 , warp
46+ , warp-tls
42 , websockets
47 , websockets
43 exposed-modules:
48 exposed-modules:
44 Obelisk.Run
49 Obelisk.Run
45 ghc-options: -Wall -fwarn-redundant-constraints
50 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+120−59
@@ -1,17 +1,17 @@
1+{-# LANGUAGE CPP #-}
2+#if defined(IPROUTE_SUPPORTED)
3+{-# LANGUAGE TemplateHaskell #-}
4+#endif
5+
1{-# LANGUAGE ConstraintKinds #-}
6{-# LANGUAGE ConstraintKinds #-}
2−{-# LANGUAGE EmptyCase #-}
3−{-# LANGUAGE EmptyDataDecls #-}
4{-# LANGUAGE FlexibleContexts #-}
7{-# LANGUAGE FlexibleContexts #-}
5{-# LANGUAGE FlexibleInstances #-}
8{-# LANGUAGE FlexibleInstances #-}
6{-# LANGUAGE GADTs #-}
9{-# LANGUAGE GADTs #-}
7−{-# LANGUAGE KindSignatures #-}
8{-# LANGUAGE LambdaCase #-}
10{-# LANGUAGE LambdaCase #-}
9{-# LANGUAGE MultiParamTypeClasses #-}
11{-# LANGUAGE MultiParamTypeClasses #-}
10{-# LANGUAGE OverloadedStrings #-}
12{-# LANGUAGE OverloadedStrings #-}
11{-# LANGUAGE Rank2Types #-}
13{-# LANGUAGE Rank2Types #-}
12−{-# LANGUAGE RecursiveDo #-}
13{-# LANGUAGE ScopedTypeVariables #-}
14{-# LANGUAGE ScopedTypeVariables #-}
14−{-# LANGUAGE TemplateHaskell #-}
15{-# LANGUAGE TypeFamilies #-}
15{-# LANGUAGE TypeFamilies #-}
16{-# LANGUAGE UndecidableInstances #-}
16{-# LANGUAGE UndecidableInstances #-}
17{-# OPTIONS_GHC -fno-warn-orphans #-} -- Due to instance HasJS x (EventWriterT t w m)
17{-# OPTIONS_GHC -fno-warn-orphans #-} -- Due to instance HasJS x (EventWriterT t w m)
@@ -25,17 +25,22 @@ import Control.Exception
25import Control.Lens ((%~), (^?), _Just, _Right)
25import Control.Lens ((%~), (^?), _Just, _Right)
26import qualified Data.Attoparsec.ByteString.Char8 as A
26import qualified Data.Attoparsec.ByteString.Char8 as A
27import Data.ByteString (ByteString)
27import Data.ByteString (ByteString)
28+import qualified Data.ByteString as BS
28import qualified Data.ByteString.Char8 as BSC
29import qualified Data.ByteString.Char8 as BSC
29import qualified Data.ByteString.Lazy.Char8 as BSLC
30import qualified Data.ByteString.Lazy.Char8 as BSLC
30−import Data.Dependent.Sum (DSum (..))
31+import qualified Data.ByteString.UTF8 as BSUTF8
31import Data.Functor.Identity
32import Data.Functor.Identity
32−import Data.Functor.Sum
33import Data.List (uncons)
33import Data.List (uncons)
34+import Data.Map (Map)
35+import qualified Data.Map as Map
34import Data.Maybe
36import Data.Maybe
35import Data.Semigroup ((<>))
37import Data.Semigroup ((<>))
36import Data.Streaming.Network (bindPortTCP)
38import Data.Streaming.Network (bindPortTCP)
39+import Data.String (fromString)
37import Data.Text (Text)
40import Data.Text (Text)
38import qualified Data.Text as T
41import qualified Data.Text as T
42+import qualified Data.Text.Encoding as T
43+import Data.Time.Clock (getCurrentTime, addUTCTime)
39import Language.Javascript.JSaddle.Run (syncPoint)
44import Language.Javascript.JSaddle.Run (syncPoint)
40import Language.Javascript.JSaddle.WebSockets
45import Language.Javascript.JSaddle.WebSockets
41import Network.HTTP.Client (Manager, defaultManagerSettings, newManager)
46import Network.HTTP.Client (Manager, defaultManagerSettings, newManager)
@@ -45,29 +50,36 @@ import Network.Socket
45import Network.Wai (Application)
50import Network.Wai (Application)
46import qualified Network.Wai as W
51import qualified Network.Wai as W
47import Network.Wai.Handler.Warp
52import Network.Wai.Handler.Warp
53+import Network.Wai.Handler.WarpTLS
48import Network.Wai.Handler.Warp.Internal (settingsHost, settingsPort)
54import Network.Wai.Handler.Warp.Internal (settingsHost, settingsPort)
49import Network.WebSockets (ConnectionOptions)
55import Network.WebSockets (ConnectionOptions)
50import Network.WebSockets.Connection (defaultConnectionOptions)
56import Network.WebSockets.Connection (defaultConnectionOptions)
51import qualified Obelisk.Asset.Serve.Snap as Snap
57import qualified Obelisk.Asset.Serve.Snap as Snap
52−import Obelisk.ExecutableConfig (get)
58+import Obelisk.Backend
53−import Obelisk.ExecutableConfig.Inject (injectExecutableConfigs)
54import Obelisk.Frontend
59import Obelisk.Frontend
55import Obelisk.Route.Frontend
60import 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
56import Reflex.Dom.Core
65import Reflex.Dom.Core
57import Snap.Core (Snap)
66import Snap.Core (Snap)
58import System.Environment
67import System.Environment
59import System.IO
68import System.IO
60import System.Process
69import System.Process
61−import System.Exit (ExitCode(..))
62import Text.URI (URI)
70import Text.URI (URI)
63import qualified Text.URI as URI
71import qualified Text.URI as URI
64import Text.URI.Lens
72import Text.URI.Lens
65−import Obelisk.Backend
73+import Web.Cookie
74+
75+#if defined(IPROUTE_SUPPORTED)
76+import qualified System.Which
77+#endif
66
78
67run
79run
68 :: Int -- ^ Port to run the backend
80 :: Int -- ^ Port to run the backend
69 -> ([Text] -> Snap ()) -- ^ Static asset handler
81 -> ([Text] -> Snap ()) -- ^ Static asset handler
70− -> Backend fullRoute frontendRoute -- ^ Backend
82+ -> Backend backendRoute frontendRoute -- ^ Backend
71 -> Frontend (R frontendRoute) -- ^ Frontend
83 -> Frontend (R frontendRoute) -- ^ Frontend
72 -> IO ()
84 -> IO ()
73run port serveStaticAsset backend frontend = do
85run port serveStaticAsset backend frontend = do
@@ -77,82 +89,126 @@ run port serveStaticAsset backend frontend = do
77 case checkEncoder $ _backend_routeEncoder backend of
89 case checkEncoder $ _backend_routeEncoder backend of
78 Left e -> hPutStrLn stderr $ "backend error:\n" <> T.unpack e
90 Left e -> hPutStrLn stderr $ "backend error:\n" <> T.unpack e
79 Right validFullEncoder -> do
91 Right validFullEncoder -> do
80− backendTid <- forkIO $ handle handleBackendErr $ withArgs ["--quiet", "--port", show port] $ do
92+ publicConfigs <- getPublicConfigs
81− _backend_run backend $ \serveRoute -> do
93+ backendTid <- forkIO $ handle handleBackendErr $ withArgs ["--quiet", "--port", show port] $
82− runSnapWithCommandLineArgs $ do
94+ _backend_run backend $ \serveRoute ->
95+ runSnapWithCommandLineArgs $
83 getRouteWith validFullEncoder >>= \case
96 getRouteWith validFullEncoder >>= \case
84 Identity r -> case r of
97 Identity r -> case r of
85− InL backendRoute :=> Identity a -> serveRoute $ backendRoute :/ a
98+ FullRoute_Backend backendRoute :/ a -> serveRoute $ backendRoute :/ a
86− InR obeliskRoute :=> Identity a ->
99+ FullRoute_Frontend obeliskRoute :/ a ->
87− serveDefaultObeliskApp (mkRouteToUrl validFullEncoder) serveStaticAsset frontend $ obeliskRoute :/ a
100+ serveDefaultObeliskApp appRouteToUrl (($ allJsUrl) <$> defaultGhcjsWidgets) serveStaticAsset frontend publicConfigs $ obeliskRoute :/ a
101+ where
102+ appRouteToUrl (k :/ v) = renderObeliskRoute validFullEncoder (FullRoute_Frontend (ObeliskRoute_App k) :/ v)
103+ allJsUrl = renderAllJsPath validFullEncoder
104+
88 let conf = defRunConfig { _runConfig_redirectPort = port }
105 let conf = defRunConfig { _runConfig_redirectPort = port }
89− runWidget conf frontend validFullEncoder `finally` killThread backendTid
106+ runWidget conf publicConfigs frontend validFullEncoder `finally` killThread backendTid
90
107
91-- Convenience wrapper to handle path segments for 'Snap.serveAsset'
108-- Convenience wrapper to handle path segments for 'Snap.serveAsset'
92runServeAsset :: FilePath -> [Text] -> Snap ()
109runServeAsset :: FilePath -> [Text] -> Snap ()
93runServeAsset rootPath = Snap.serveAsset "" rootPath . T.unpack . T.intercalate "/"
110runServeAsset rootPath = Snap.serveAsset "" rootPath . T.unpack . T.intercalate "/"
94
111
95−getConfigRoute :: IO (Maybe URI)
112+getConfigRoute :: Map Text ByteString -> Either Text URI
96−getConfigRoute = get "config/common/route" >>= \case
113+getConfigRoute configs = case Map.lookup "common/route" configs of
97− Just r -> case URI.mkURI $ T.strip r of
114+ Just r ->
98− Just route -> pure $ Just route
115+ let stripped = T.strip (T.decodeUtf8 r)
99− Nothing -> do
116+ in case URI.mkURI stripped of
100− putStrLn $ "Route is invalid: " <> show r
117+ Just route -> Right route
101− pure Nothing
118+ Nothing -> Left $ "Couldn't parse route as URI; value read was: " <> T.pack (show stripped)
102− Nothing -> pure Nothing
119+ Nothing -> Left $ "Couldn't find config file common/route; it should contain the site's canonical root URI" <> T.pack (show $ Map.keys configs)
103−
120+
104−defAppUri :: URI
121+runWidget
105−defAppUri = fromMaybe (error "defAppUri") $ URI.mkURI "http://127.0.0.1:8000"
122+ :: RunConfig
106−
123+ -> Map Text ByteString
107−runWidget :: RunConfig -> Frontend (R route) -> Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName -> IO ()
124+ -> Frontend (R frontendRoute)
108−runWidget conf frontend validFullEncoder = do
125+ -> Encoder Identity Identity (R (FullRoute backendRoute frontendRoute)) PageName
109− uri <- fromMaybe defAppUri <$> getConfigRoute
126+ -> IO ()
127+runWidget conf configs frontend validFullEncoder = do
128+ uri <- either (fail . T.unpack) pure $ getConfigRoute configs
110 let port = fromIntegral $ fromMaybe 80 $ uri ^? uriAuthority . _Right . authPort . _Just
129 let port = fromIntegral $ fromMaybe 80 $ uri ^? uriAuthority . _Right . authPort . _Just
111 redirectHost = _runConfig_redirectHost conf
130 redirectHost = _runConfig_redirectHost conf
112 redirectPort = _runConfig_redirectPort conf
131 redirectPort = _runConfig_redirectPort conf
113 beforeMainLoop = do
132 beforeMainLoop = do
114 putStrLn $ "Frontend running on " <> T.unpack (URI.render uri)
133 putStrLn $ "Frontend running on " <> T.unpack (URI.render uri)
115 settings = setBeforeMainLoop beforeMainLoop (setPort port (setTimeout 3600 defaultSettings))
134 settings = setBeforeMainLoop beforeMainLoop (setPort port (setTimeout 3600 defaultSettings))
135+ -- Providing TLS here will also incidentally provide it to proxied requests to the backend.
136+ prepareRunner = case uri ^? uriScheme . _Just . unRText of
137+ Just "https" -> do
138+ -- Generate a private key and self-signed certificate for TLS
139+ privateKey <- RSA.generateRSAKey' 2048 3
140+
141+ certRequest <- X509Request.newX509Req
142+ _ <- X509Request.setPublicKey certRequest privateKey
143+ _ <- X509Request.signX509Req certRequest privateKey Nothing
144+
145+ cert <- X509.newX509 >>= X509Request.makeX509FromReq certRequest
146+ _ <- X509.setPublicKey cert privateKey
147+ now <- getCurrentTime
148+ _ <- X509.setNotBefore cert $ addUTCTime (-1) now
149+ _ <- X509.setNotAfter cert $ addUTCTime (365 * 24 * 60 * 60) now
150+ _ <- X509.signX509 cert privateKey Nothing
151+
152+ certByteString <- BSUTF8.fromString <$> PEM.writeX509 cert
153+ privateKeyByteString <- BSUTF8.fromString <$> PEM.writePKCS8PrivateKey privateKey Nothing
154+
155+ return $ runTLSSocket (tlsSettingsMemory certByteString privateKeyByteString)
156+ _ -> return runSettingsSocket
157+ runner <- prepareRunner
116 bracket
158 bracket
117 (bindPortTCPRetry settings (logPortBindErr port) (_runConfig_retryTimeout conf))
159 (bindPortTCPRetry settings (logPortBindErr port) (_runConfig_retryTimeout conf))
118 close
160 close
119 (\skt -> do
161 (\skt -> do
120 man <- newManager defaultManagerSettings
162 man <- newManager defaultManagerSettings
121− app <- obeliskApp defaultConnectionOptions frontend validFullEncoder uri $ fallbackProxy redirectHost redirectPort man
163+ app <- obeliskApp configs defaultConnectionOptions frontend validFullEncoder uri $ fallbackProxy redirectHost redirectPort man
122− runSettingsSocket settings skt app)
164+ runner settings skt app)
123
165
124obeliskApp
166obeliskApp
125− :: forall route backendRoute. ConnectionOptions
167+ :: forall frontendRoute backendRoute
126− -> Frontend (R route)
168+ . Map Text ByteString
127− -> Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName
169+ -> ConnectionOptions
170+ -> Frontend (R frontendRoute)
171+ -> Encoder Identity Identity (R (FullRoute backendRoute frontendRoute)) PageName
128 -> URI
172 -> URI
129 -> Application
173 -> Application
130 -> IO Application
174 -> IO Application
131−obeliskApp opts frontend validFullEncoder uri backend = do
175+obeliskApp configs opts frontend validFullEncoder uri backend = do
132− let entryPoint = do
176+ let mode = FrontendMode
133− runFrontend validFullEncoder frontend
177+ { _frontendMode_hydrate = True
178+ , _frontendMode_adjustRoute = False
179+ }
180+ entryPoint = do
181+ runFrontendWithConfigsAndCurrentRoute mode configs validFullEncoder frontend
134 syncPoint
182 syncPoint
135 jsaddlePath <- URI.mkPathPiece "jsaddle"
183 jsaddlePath <- URI.mkPathPiece "jsaddle"
136 let jsaddleUri = BSLC.fromStrict $ URI.renderBs $ uri & uriPath %~ (<>[jsaddlePath])
184 let jsaddleUri = BSLC.fromStrict $ URI.renderBs $ uri & uriPath %~ (<>[jsaddlePath])
137 Right (jsaddleWarpRouteValidEncoder :: Encoder Identity (Either Text) (R JSaddleWarpRoute) PageName) <- return $ checkEncoder jsaddleWarpRouteEncoder
185 Right (jsaddleWarpRouteValidEncoder :: Encoder Identity (Either Text) (R JSaddleWarpRoute) PageName) <- return $ checkEncoder jsaddleWarpRouteEncoder
138 jsaddle <- jsaddleWithAppOr opts entryPoint $ \_ sendResponse -> sendResponse $ W.responseLBS H.status500 [("Content-Type", "text/plain")] "obeliskApp: jsaddle got a bad URL"
186 jsaddle <- jsaddleWithAppOr opts entryPoint $ \_ sendResponse -> sendResponse $ W.responseLBS H.status500 [("Content-Type", "text/plain")] "obeliskApp: jsaddle got a bad URL"
139− return $ \req sendResponse -> case tryDecode validFullEncoder (W.pathInfo req, mempty) of --TODO: Query strings
187+ return $ \req sendResponse -> case tryDecode validFullEncoder $ byteStringsToPageName (BS.dropWhile (== (fromIntegral $ fromEnum '/')) $ W.rawPathInfo req) (BS.drop 1 $ W.rawQueryString req) of
140 Identity r -> case r of
188 Identity r -> case r of
141− InR (ObeliskRoute_Resource ResourceRoute_JSaddleWarp) :=> Identity jsaddleRoute -> case jsaddleRoute of
189+ FullRoute_Frontend (ObeliskRoute_Resource ResourceRoute_JSaddleWarp) :/ jsaddleRoute -> case jsaddleRoute of
142 JSaddleWarpRoute_JavaScript :/ () -> sendResponse $ W.responseLBS H.status200 [("Content-Type", "application/javascript")] $ jsaddleJs' (Just jsaddleUri) False
190 JSaddleWarpRoute_JavaScript :/ () -> sendResponse $ W.responseLBS H.status200 [("Content-Type", "application/javascript")] $ jsaddleJs' (Just jsaddleUri) False
143 _ -> flip jsaddle sendResponse $ req
191 _ -> flip jsaddle sendResponse $ req
144 { W.pathInfo = fst $ encode jsaddleWarpRouteValidEncoder jsaddleRoute
192 { W.pathInfo = fst $ encode jsaddleWarpRouteValidEncoder jsaddleRoute
145 }
193 }
146− InR (ObeliskRoute_App appRouteComponent) :=> Identity appRouteRest -> do
194+ FullRoute_Frontend (ObeliskRoute_App appRouteComponent) :/ appRouteRest -> do
147− html <- renderJsaddleFrontend (mkRouteToUrl validFullEncoder) (appRouteComponent :/ appRouteRest) frontend
195+ let cookies = maybe [] parseCookies $ lookup (fromString "Cookie") (W.requestHeaders req)
196+ routeToUrl (k :/ v) = renderObeliskRoute validFullEncoder $ FullRoute_Frontend (ObeliskRoute_App k) :/ v
197+ html <- renderJsaddleFrontend configs cookies routeToUrl (appRouteComponent :/ appRouteRest) frontend
148 sendResponse $ W.responseLBS H.status200 [("Content-Type", staticRenderContentType)] $ BSLC.fromStrict html
198 sendResponse $ W.responseLBS H.status200 [("Content-Type", staticRenderContentType)] $ BSLC.fromStrict html
149 _ -> backend req sendResponse
199 _ -> backend req sendResponse
150
200
151−renderJsaddleFrontend :: (route -> Text) -> route -> Frontend route -> IO ByteString
201+renderJsaddleFrontend
152−renderJsaddleFrontend urlEnc r f =
202+ :: Map Text ByteString
203+ -> Cookies
204+ -> (route -> Text)
205+ -> route
206+ -> Frontend route
207+ -> IO ByteString
208+renderJsaddleFrontend configs cookies urlEnc r f =
153 let jsaddleScript = elAttr "script" ("src" =: "/jsaddle/jsaddle.js") blank
209 let jsaddleScript = elAttr "script" ("src" =: "/jsaddle/jsaddle.js") blank
154 jsaddlePreload = elAttr "link" ("rel" =: "preload" <> "as" =: "script" <> "href" =: "/jsaddle/jsaddle.js") blank
210 jsaddlePreload = elAttr "link" ("rel" =: "preload" <> "as" =: "script" <> "href" =: "/jsaddle/jsaddle.js") blank
155− in renderFrontendHtml urlEnc r (_frontend_head f >> injectExecutableConfigs >> jsaddlePreload) (_frontend_body f >> jsaddleScript)
211+ in renderFrontendHtml configs cookies urlEnc r f jsaddlePreload jsaddleScript
156
212
157-- | like 'bindPortTCP' but reconnects on exception
213-- | like 'bindPortTCP' but reconnects on exception
158bindPortTCPRetry :: Settings
214bindPortTCPRetry :: Settings
@@ -169,17 +225,22 @@ logPortBindErr p e = getProcessIdForPort p >>= \case
169 Nothing -> putStrLn $ "runWidget: " <> show e
225 Nothing -> putStrLn $ "runWidget: " <> show e
170 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."]
226 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."]
171
227
228+ssPath :: Maybe String
229+ssPath =
230+#if defined(IPROUTE_SUPPORTED)
231+ Just $(System.Which.staticWhich "ss")
232+#else
233+ Nothing
234+#endif
235+
172getProcessIdForPort :: Int -> IO (Maybe Int)
236getProcessIdForPort :: Int -> IO (Maybe Int)
173−getProcessIdForPort port = do
237+getProcessIdForPort port = case ssPath of
174− -- First check if 'ss' is available
238+ Just ss -> do
175− (c, _, _) <- readProcessWithExitCode "which" ["ss"] mempty
239+ xs <- lines <$> readProcess ss ["-lptn", "sport = " <> show port] mempty
176− case c of
240+ case uncons xs of
177− ExitSuccess -> do
241+ Just (_, x:_) -> return $ A.maybeResult $ A.parse parseSsPid $ BSC.pack x
178− xs <- lines <$> readProcess "ss" ["-lptn", "sport = " <> show port] mempty
242+ _ -> return Nothing
179− case uncons xs of
243+ _ -> return Nothing
180− Just (_, x:_) -> return $ A.maybeResult $ A.parse parseSsPid $ BSC.pack x
181− _ -> return Nothing
182− _ -> return Nothing
183
244
184parseSsPid :: A.Parser Int
245parseSsPid :: A.Parser Int
185parseSsPid = do
246parseSsPid = do
lib/selftest/obelisk-selftest.cabal+5−3
@@ -11,15 +11,17 @@ library
11 base
11 base
12 , containers
12 , containers
13 , directory
13 , directory
14+ , filepath
14 , hspec
15 , hspec
15 , http-client
16 , http-client
16 , http-types
17 , http-types
17 , HUnit
18 , HUnit
18− , obelisk-cliapp
19+ , network
20+ , obelisk-executable-config-lookup
19 , obelisk-run
21 , obelisk-run
22+ , which
20 , process
23 , process
21− , network
24+ , shelly >= 1.9.0
22− , shelly
23 , temporary
25 , temporary
24 , text
26 , text
25 exposed-modules:
27 exposed-modules:
lib/selftest/src/Obelisk/SelfTest.hs+325−191
@@ -1,39 +1,43 @@
1+{-# LANGUAGE DoAndIfThenElse #-}
1{-# LANGUAGE EmptyCase #-}
2{-# LANGUAGE EmptyCase #-}
2{-# LANGUAGE LambdaCase #-}
3{-# LANGUAGE LambdaCase #-}
3{-# LANGUAGE OverloadedStrings #-}
4{-# LANGUAGE OverloadedStrings #-}
4{-# LANGUAGE PatternGuards #-}
5{-# LANGUAGE PatternGuards #-}
5{-# LANGUAGE ScopedTypeVariables #-}
6{-# LANGUAGE ScopedTypeVariables #-}
7+{-# LANGUAGE TemplateHaskell #-}
6{-# LANGUAGE TypeApplications #-}
8{-# LANGUAGE TypeApplications #-}
9+{-# LANGUAGE ViewPatterns #-}
7module Obelisk.SelfTest where
10module Obelisk.SelfTest where
8
11
9−import Control.Exception (bracket, throw)
12+import Control.Concurrent (threadDelay)
13+import Control.Exception (bracket, throw, try)
10import Control.Monad
14import Control.Monad
11import Control.Monad.IO.Class
15import Control.Monad.IO.Class
12import Data.Bool (bool)
16import Data.Bool (bool)
13import Data.Function (fix)
17import Data.Function (fix)
14−import Data.Semigroup (Semigroup, (<>))
18+import Data.Semigroup ((<>))
15import qualified Data.Set as Set
19import qualified Data.Set as Set
16import Data.String
20import Data.String
17import Data.Text (Text)
21import Data.Text (Text)
18−import Data.Void
19import qualified Data.Text as T
22import qualified Data.Text as T
20import qualified Data.Text.IO as T
23import qualified Data.Text.IO as T
21import qualified Network.HTTP.Client as HTTP
24import qualified Network.HTTP.Client as HTTP
22import qualified Network.HTTP.Types as HTTP
25import qualified Network.HTTP.Types as HTTP
23import qualified Network.Socket as Socket
26import qualified Network.Socket as Socket
24import Shelly
27import Shelly
25−import System.Directory (withCurrentDirectory, getDirectoryContents)
28+import System.Directory (getCurrentDirectory, withCurrentDirectory, getDirectoryContents)
26import System.Environment
29import System.Environment
27import System.Exit (ExitCode (..))
30import System.Exit (ExitCode (..))
28−import System.Info
31+import System.FilePath (addTrailingPathSeparator)
29−import System.IO (Handle, hClose)
32+import qualified System.Info
33+import System.IO (Handle, hClose, hIsEOF, hGetContents)
30import System.IO.Temp
34import System.IO.Temp
31−import System.Process (readProcessWithExitCode, CreateProcess(cwd), readCreateProcessWithExitCode, proc)
35+import System.Process (readProcessWithExitCode)
36+import System.Which (staticWhich)
32import Test.Hspec
37import Test.Hspec
33import Test.HUnit.Base
38import Test.HUnit.Base
34
39
35−import Obelisk.CliApp hiding (runCli, readCreateProcessWithExitCode)
40+import Obelisk.ExecutableConfig.Lookup (getConfigs)
36−import qualified Obelisk.CliApp as CliApp
37import Obelisk.Run (getConfigRoute)
41import Obelisk.Run (getConfigRoute)
38
42
39data ObRunState
43data ObRunState
@@ -42,24 +46,47 @@ data ObRunState
42 | ObRunState_BackendStarted
46 | ObRunState_BackendStarted
43 deriving (Eq, Show)
47 deriving (Eq, Show)
44
48
45−doubleQuotes :: (IsString a, Semigroup a) => a -> a
49+cpPath :: FilePath
46−doubleQuotes s = "\"" <> s <> "\""
50+cpPath = $(staticWhich "cp")
51+
52+cabalPath :: FilePath
53+cabalPath = $(staticWhich "cabal")
54+
55+gitPath :: FilePath
56+gitPath = $(staticWhich "git")
57+
58+chownPath :: FilePath
59+chownPath = $(staticWhich "chown")
60+
61+chmodPath :: FilePath
62+chmodPath = $(staticWhich "chmod")
63+
64+whoamiPath :: FilePath
65+whoamiPath = $(staticWhich "whoami")
66+
67+nixBuildPath :: FilePath
68+nixBuildPath = $(staticWhich "nix-build")
69+
70+lnPath :: FilePath
71+lnPath = $(staticWhich "ln")
72+
73+rmPath :: FilePath
74+rmPath = $(staticWhich "rm")
75+
76+rsyncPath :: FilePath
77+rsyncPath = $(staticWhich "rsync")
78+
79+gitUserConfig :: [Text]
80+gitUserConfig = ["-c", "user.name=Obelisk Selftest", "-c", "user.email=noreply@example.com"]
47
81
48commit :: Text -> Sh ()
82commit :: Text -> Sh ()
49−commit msg = void $ run "git"
83+commit msg = void $ run gitPath $ gitUserConfig <> [ "commit"
50− [ "commit"
51 , "--no-gpg-sign"
84 , "--no-gpg-sign"
52 , "--allow-empty"
85 , "--allow-empty"
53 , "-m"
86 , "-m"
54− , doubleQuotes msg
87+ , msg
55 ]
88 ]
56
89
57−-- TODO replace Void and stop using loosely typed synchronous exceptions
58−runCli :: MonadIO m => CliT Void IO a -> m a
59−runCli f = liftIO $ do
60− c <- newCliConfig Notice False False (\case {})
61− CliApp.runCli c f
62−
63tshow :: Show a => a -> Text
90tshow :: Show a => a -> Text
64tshow = T.pack . show
91tshow = T.pack . show
65
92
@@ -67,194 +94,294 @@ tshow = T.pack . show
67shellyOb :: MonadIO m => (Sh a -> Sh a) -> Sh a -> m a
94shellyOb :: MonadIO m => (Sh a -> Sh a) -> Sh a -> m a
68shellyOb f obTest = shelly $ f obTest
95shellyOb f obTest = shelly $ f obTest
69
96
97+-- Set "ob" in a single place so it can be replaced with a
98+-- link to obelisk in the nix store in the future,
99+-- and avoid PATH hacking before calling this script.
100+ob :: FilePath
101+ob = "ob"
102+
103+augmentWithVerbosity :: (String -> [Text] -> a) -> String -> Bool -> [Text] -> a
104+augmentWithVerbosity runner executable isVerbose args = runner executable $ (if isVerbose then ("-v" :) else id) args
105+
106+-- | Copies a git repo to a new location and "resets" the git history to include
107+-- exactly one commit with all files added. It then restricts writing and reading
108+-- for group and user to make the repo ideal for being a valid git remote for thunks.
109+--
110+-- Using this allows dirty repos to be used as git remotes during the test since 'git clone'ing
111+-- a dirty repo will not include the uncommitted changes.
112+copyForGitRemote :: Bool -> FilePath -> FilePath -> IO ()
113+copyForGitRemote isVerbose origDir copyDir = shelly $ bool silently verbosely isVerbose $ do
114+ setenv "HOME" "/dev/null"
115+ setenv "GIT_CONFIG_NOSYSTEM" "1"
116+ run_ rsyncPath
117+ [ "-r", "--no-perms", "--no-owner", "--no-group", "--exclude", ".git"
118+ , toTextIgnore (addTrailingPathSeparator origDir), toTextIgnore copyDir
119+ ]
120+ git ["init"]
121+ git ["config", "user.name", "SelfTest"]
122+ git ["config", "user.email", "self@test"]
123+ git ["add", "--all"]
124+ git ["commit", "-m", "Copy repo"]
125+ run_ chmodPath ["-R", "u-w,g-rw,o-rw", toTextIgnore copyDir] -- Freeze this state
126+ where
127+ git args = run_ gitPath $ ["-C", toTextIgnore copyDir] <> args
128+
70main :: IO ()
129main :: IO ()
71main = do
130main = do
72 -- Note: you can pass hspec arguments as well, eg: `-m <pattern>`
131 -- Note: you can pass hspec arguments as well, eg: `-m <pattern>`
73− isVerbose <- (elem "-v") <$> getArgs
132+ isVerbose <- elem "-v" <$> getArgs
74 unless isVerbose $
133 unless isVerbose $
75 putStrLn "Tests may take longer to run if there are unbuilt derivations: use -v for verbose output"
134 putStrLn "Tests may take longer to run if there are unbuilt derivations: use -v for verbose output"
76− let verbosity = bool silently verbosely isVerbose
135+
77− obeliskImpl <- fromString <$> getEnv "OBELISK_IMPL"
136+ obeliskImplDirtyReadOnly <- getCurrentDirectory
78 httpManager <- HTTP.newManager HTTP.defaultManagerSettings
137 httpManager <- HTTP.newManager HTTP.defaultManagerSettings
79− [p0, p1, p2, p3] <- liftIO $ getFreePorts 4
80− withSystemTempDirectory "initCache" $ \initCache -> do
81− -- Setup the ob init cache
82− void . shellyOb verbosity $ chdir (fromString initCache) $ do
83− run_ "ob" ["init"]
84− run_ "git" ["init"]
85− hspec $ parallel $ do
86− let shelly_ = void . shellyOb verbosity
87−
88− inTmp :: (Shelly.FilePath -> Sh a) -> IO ()
89− inTmp f = withTmp (chdir <*> f)
90−
91− withTmp f = shelly_ . withSystemTempDirectory "test" $ f . fromString
92−
93− inTmpObInit f = inTmp $ \dir -> do
94− run_ "cp" ["-a", fromString $ initCache <> "/.", toTextIgnore dir]
95− f dir
96−
97− assertRevEQ a b = liftIO . assertEqual "" "" =<< diff a b
98− assertRevNE a b = liftIO . assertBool "" . (/= "") =<< diff a b
99−
100− revParseHead = T.strip <$> run "git" ["rev-parse", "HEAD"]
101−
102− commitAll = do
103− run_ "git" ["add", "."]
104− commit "checkpoint"
105− revParseHead
106−
107− thunk = ".obelisk/impl"
108− update = run "ob" ["thunk", "update", thunk] >> commitAll
109− pack = run "ob" ["thunk", "pack", thunk] >> commitAll
110− unpack = run "ob" ["thunk", "unpack", thunk] >> commitAll
111−
112− diff a b = run "git" ["diff", a, b]
113−
114− describe "ob init" $ parallel $ do
115− it "works with default impl" $ inTmp $ \_ -> run "ob" ["init"]
116− it "works with master branch impl" $ inTmp $ \_ -> run "ob" ["init", "--branch", "master"]
117− it "works with symlink" $ inTmp $ \_ -> run "ob" ["init", "--symlink", obeliskImpl]
118− it "doesn't silently overwrite existing files" $ withSystemTempDirectory "ob-init" $ \dir -> do
119− let p force = (proc "ob" $ "--no-handoff" : "init" : if force then ["--force"] else []) { cwd = Just dir }
120− (ExitSuccess, _, _) <- readCreateProcessWithExitCode (p False) ""
121− (ExitFailure _, _, _) <- readCreateProcessWithExitCode (p False) ""
122− (ExitSuccess, _, _) <- readCreateProcessWithExitCode (p True) ""
123− pure ()
124−
125− it "doesn't create anything when given an invalid impl" $ inTmp $ \tmp -> do
126− void $ errExit False $ run "ob" ["init", "--symlink", "/dev/null"]
127− ls tmp >>= liftIO . assertEqual "" []
128−
129− it "produces a valid route config" $ inTmpObInit $ \tmp -> do
130− liftIO $ withCurrentDirectory (T.unpack $ toTextIgnore tmp) $ getConfigRoute `shouldNotReturn` Nothing
131−
132− -- These tests fail with "Could not find module 'Obelisk.Generated.Static'"
133− -- when not run by 'nix-build --attr selftest'
134− describe "ob run" $ parallel $ do
135− it "works in root directory" $ inTmpObInit $ \_ -> do
136− testObRunInDir p0 p1 Nothing httpManager
137− it "works in sub directory" $ inTmpObInit $ \_ -> do
138− testObRunInDir p2 p3 (Just "frontend") httpManager
139−
140− describe "obelisk project" $ parallel $ do
141− it "can build obelisk command" $ inTmpObInit $ \_ -> run "nix-build" ["-A", "command" , obeliskImpl]
142− it "can build obelisk skeleton" $ inTmpObInit $ \_ -> run "nix-build" ["-A", "skeleton", obeliskImpl]
143− it "can build obelisk shell" $ inTmpObInit $ \_ -> run "nix-build" ["-A", "shell", obeliskImpl]
144− -- See https://github.com/obsidiansystems/obelisk/issues/101
145− -- it "can build everything" $ shelly_ $ run "nix-build" [obeliskImpl]
146−
147− describe "blank initialized project" $ parallel $ do
148−
149− it "can build ghc.backend" $ inTmpObInit $ \_ -> do
150− run "nix-build" ["--no-out-link", "-A", "ghc.backend"]
151− it "can build ghcjs.frontend" $ inTmpObInit $ \_ -> do
152− run "nix-build" ["--no-out-link", "-A", "ghcjs.frontend"]
153−
154− if os == "darwin"
155− then it "can build ios" $ inTmpObInit $ \_ -> run "nix-build" ["--no-out-link", "-A", "ios.frontend" ]
156− else it "can build android" $ inTmpObInit $ \_ -> run "nix-build" ["--no-out-link", "-A", "android.frontend"]
157−
158− forM_ ["ghc", "ghcjs"] $ \compiler -> do
159− let
160− shell = "shells." <> compiler
161− inShell cmd' = run "nix-shell" ["-A", fromString shell, "--run", cmd']
162− it ("can enter " <> shell) $ inTmpObInit $ \_ -> inShell "exit"
163− it ("can build in " <> shell) $ inTmpObInit $ \_ -> inShell $ "cabal new-build --" <> fromString compiler <> " all"
164−
165− it "can build reflex project" $ inTmpObInit $ \_ -> do
166− run "nix-build" []
167−
168− it "has idempotent thunk update" $ inTmpObInit $ \_ -> do
169− u <- update
170− uu <- update
171− assertRevEQ u uu
172−
173− describe "ob thunk pack/unpack" $ parallel $ do
174− it "has thunk pack and unpack inverses" $ inTmpObInit $ \_ -> do
175−
176− e <- commitAll
177− eu <- unpack
178− eup <- pack
179− eupu <- unpack
180− _ <- pack
181−
182− assertRevEQ e eup
183− assertRevEQ eu eupu
184− assertRevNE e eu
185−
186− it "unpacks the correct branch" $ withTmp $ \dir -> do
187− let branch = "master"
188− run_ "git" ["clone", "https://github.com/reflex-frp/reflex.git", toTextIgnore dir, "--branch", branch]
189− run_ "ob" ["thunk", "pack", toTextIgnore dir]
190− run_ "ob" ["thunk", "unpack", toTextIgnore dir]
191− branch' <- chdir dir $ run "git" ["rev-parse", "--abbrev-ref", "HEAD"]
192− liftIO $ assertEqual "" branch (T.strip branch')
193−
194− it "can pack and unpack plain git repos" $ do
195− shelly_ $ withSystemTempDirectory "git-repo" $ \dir -> do
196− let repo = toTextIgnore $ dir </> ("repo" :: String)
197− run_ "git" ["clone", "https://github.com/haskell/process.git", repo]
198− origHash <- chdir (fromText repo) revParseHead
199−
200− run_ "ob" ["thunk", "pack", repo]
201− packedFiles <- Set.fromList <$> ls (fromText repo)
202− liftIO $ assertEqual "" packedFiles $ Set.fromList $ (repo </>) <$>
203− ["default.nix", "github.json", ".attr-cache" :: String]
204−
205− run_ "ob" ["thunk", "unpack", repo]
206− chdir (fromText repo) $ do
207− unpackHash <- revParseHead
208− assertRevEQ origHash unpackHash
209−
210− testThunkPack $ fromText repo
211−
212− it "aborts thunk pack when there are uncommitted files" $ inTmpObInit $ \dir -> do
213− void $ unpack
214− testThunkPack (dir </> thunk)
215−
216− describe "ob thunk update --branch" $ parallel $ do
217− it "can change a thunk to the latest version of a desired branch" $ withTmp $ \dir -> do
218− let branch1 = "master"
219− branch2 = "develop"
220− run_ "git" ["clone", "https://github.com/reflex-frp/reflex.git", toTextIgnore dir, "--branch", branch1]
221− run_ "ob" ["thunk" , "pack", toTextIgnore dir]
222− run_ "ob" ["thunk", "update", toTextIgnore dir, "--branch", branch2]
223−
224− it "doesn't create anything when given an invalid branch" $ withTmp $ \dir -> do
225− let checkDir dir' = liftIO $ getDirectoryContents $ T.unpack $ toTextIgnore dir'
226− run_ "git" ["clone", "https://github.com/reflex-frp/reflex.git", toTextIgnore dir, "--branch", "master"]
227− run_ "ob" ["thunk" , "pack", toTextIgnore dir]
228− startingContents <- checkDir dir
229− void $ errExit False $ run "ob" ["thunk", "update", toTextIgnore dir, "--branch", "dumble-palooza"]
230− checkDir dir >>= liftIO . assertEqual "" startingContents
231
138
139+ withSystemTempDirectory "obelisk-repo-git-remote" $ \copyDir -> do
140+ copyForGitRemote isVerbose obeliskImplDirtyReadOnly copyDir
141+ main' isVerbose httpManager copyDir
142+
143+main' :: Bool -> HTTP.Manager -> FilePath -> IO ()
144+main' isVerbose httpManager obeliskRepoReadOnly = withInitCache $ \initCache -> hspec $ parallel $ do
145+ let
146+ inTmpObInit' dirname f = inTmp' dirname $ \dir -> do
147+ run_ cpPath ["-rT", fromString initCache, toTextIgnore dir]
148+ f dir
149+ inTmpObInit = inTmpObInit' defaultTmpDirName
150+
151+ -- To be used in tests that change the obelisk impl directory
152+ inTmpObInitWithImplCopy f = inTmpObInit $ \dir ->
153+ withObeliskImplClean $ \(fromString -> implClean) -> do
154+ run_ rmPath [thunk]
155+ run_ lnPath ["-s", implClean, thunk]
156+ f dir
157+
158+ describe "ob init" $ parallel $ do
159+ it "works with default impl" $ inTmp $ \_ -> runOb ["init"]
160+ it "works with master branch impl" $ inTmp $ \_ -> runOb ["init", "--branch", "master"]
161+ it "works with symlink" $ inTmp $ \_ -> runOb ["init", "--symlink", toTextIgnore obeliskRepoReadOnly]
162+ it "doesn't silently overwrite existing files" $ inTmp $ \_ -> do
163+ let p force = errExit False $ do
164+ run_ ob $ "--no-handoff" : "-v" : "init" : ["--force"|force]
165+ (== 0) <$> lastExitCode
166+
167+ True <- p False
168+ False <- p False
169+ True <- p True
170+ pure ()
171+
172+ it "doesn't create anything when given an invalid impl" $ inTmp $ \tmp -> do
173+ void $ errExit False $ runOb ["init", "--symlink", "/dev/null"]
174+ ls tmp >>= liftIO . assertEqual "" []
175+
176+ it "produces a valid route config" $ inTmpObInit $ \tmp -> liftIO $
177+ withCurrentDirectory (T.unpack $ toTextIgnore tmp) $ do
178+ configs <- getConfigs
179+ return (either (const Nothing) Just $ getConfigRoute configs) `shouldNotReturn` Nothing
180+
181+ -- These tests fail with "Could not find module 'Obelisk.Generated.Static'"
182+ -- when not run by 'nix-build --attr selftest'
183+ describe "ob run" $ {- NOT parallel $ -} do
184+ it "works in root directory" $ inTmpObInit $ \_ -> testObRunInDir' Nothing httpManager
185+ it "works in sub directory" $ inTmpObInit $ \_ -> testObRunInDir' (Just "frontend") httpManager
186+
187+ describe "obelisk project" $ parallel $ do
188+ it "can build obelisk command" $ inTmpObInit $ \_ -> nixBuild ["-A", "command" , toTextIgnore obeliskRepoReadOnly]
189+ it "can build obelisk skeleton" $ inTmpObInit $ \_ -> nixBuild ["-A", "skeleton", toTextIgnore obeliskRepoReadOnly]
190+ it "can build obelisk shell" $ inTmpObInit $ \_ -> nixBuild ["-A", "shell", toTextIgnore obeliskRepoReadOnly]
191+ it "can build everything" $ inTmpObInit $ \_ -> nixBuild [toTextIgnore obeliskRepoReadOnly]
192+
193+ describe "blank initialized project" $ parallel $ do
194+
195+ it "can build ghc.backend" $ inTmpObInit $ \_ -> nixBuild ["-A", "ghc.backend"]
196+ it "can build ghcjs.frontend" $ inTmpObInit $ \_ -> nixBuild ["-A", "ghcjs.frontend"]
197+
198+ if System.Info.os == "darwin"
199+ then it "can build ios" $ inTmpObInit $ \_ -> nixBuild ["-A", "ios.frontend"]
200+ else it "can build android after accepting license" $ inTmpObInit $ \dir -> do
201+ let defaultNixPath = dir </> ("default.nix" :: FilePath)
202+ writefile defaultNixPath
203+ =<< T.replace
204+ "# config.android_sdk.accept_license = false;"
205+ "config.android_sdk.accept_license = true;"
206+ <$> readfile defaultNixPath
207+ nixBuild ["-A", "android.frontend"]
208+
209+ forM_ ["ghc", "ghcjs"] $ \compiler -> do
210+ let
211+ shellName = "shells." <> compiler
212+ inShell cmd' = run_ "nix-shell" ["default.nix", "-A", fromString shellName, "--run", cmd']
213+ it ("can enter " <> shellName) $ inTmpObInit $ \_ -> inShell "exit"
214+ -- NOTE: We override the temporary directory name because cabal has a bug preventing new-build from working
215+ -- in a path that has unicode characters.
216+ it ("can build in " <> shellName) $ inTmpObInit' "test" $ \_ -> inShell $
217+ T.pack cabalPath <> " --version; " <> T.pack cabalPath <> " new-build --" <> T.pack compiler <> " all"
218+
219+ it "has idempotent thunk update" $ inTmpObInitWithImplCopy $ \_ -> do
220+ _ <- pack
221+ u <- update
222+ uu <- update
223+ assertRevEQ u uu
224+
225+ describe "ob thunk pack/unpack" $ parallel $ do
226+ it "has thunk pack and unpack inverses" $ inTmpObInitWithImplCopy $ \_ -> do
227+
228+ _ <- pack
229+ e <- commitAll
230+ eu <- unpack
231+ eup <- pack
232+ eupu <- unpack
233+ _ <- pack
234+
235+ assertRevEQ e eup
236+ assertRevEQ eu eupu
237+ assertRevNE e eu
238+
239+ it "unpacks the correct branch" $ withTmp $ \dir -> do
240+ let branch = "master"
241+ run_ gitPath ["clone", "https://github.com/reflex-frp/reflex.git", toTextIgnore dir, "--branch", branch]
242+ runOb_ ["thunk", "pack", toTextIgnore dir]
243+ runOb_ ["thunk", "unpack", toTextIgnore dir]
244+ branch' <- chdir dir $ run gitPath ["rev-parse", "--abbrev-ref", "HEAD"]
245+ liftIO $ assertEqual "" branch (T.strip branch')
246+
247+ it "can pack and unpack plain git repos" $
248+ shelly_ $ withSystemTempDirectory "git-repo" $ \dir -> do
249+ let repo = toTextIgnore $ dir </> ("repo" :: FilePath)
250+ run_ gitPath ["clone", "https://github.com/haskell/process.git", repo]
251+ origHash <- chdir (fromText repo) revParseHead
252+
253+ runOb_ ["thunk", "pack", repo]
254+ packedFiles <- Set.fromList <$> ls (fromText repo)
255+ liftIO $ assertEqual "" packedFiles $ Set.fromList $ (repo </>) <$>
256+ ["default.nix", "github.json", ".attr-cache" :: FilePath]
257+
258+ runOb_ ["thunk", "unpack", repo]
259+ chdir (fromText repo) $ do
260+ unpackHash <- revParseHead
261+ assertRevEQ origHash unpackHash
262+
263+ testThunkPack' $ fromText repo
264+
265+ it "aborts thunk pack when there are uncommitted files" $ inTmpObInitWithImplCopy $ \dir -> do
266+ testThunkPack' (dir </> thunk)
267+
268+ describe "ob thunk update --branch" $ parallel $ do
269+ it "can change a thunk to the latest version of a desired branch" $ withTmp $ \dir -> do
270+ let branch1 = "master"
271+ branch2 = "develop"
272+ run_ gitPath ["clone", "https://github.com/reflex-frp/reflex.git", toTextIgnore dir, "--branch", branch1]
273+ runOb_ ["thunk" , "pack", toTextIgnore dir]
274+ runOb_ ["thunk", "update", toTextIgnore dir, "--branch", branch2]
275+
276+ it "doesn't create anything when given an invalid branch" $ withTmp $ \dir -> do
277+ let checkDir dir' = liftIO $ getDirectoryContents $ T.unpack $ toTextIgnore dir'
278+ run_ gitPath ["clone", "https://github.com/reflex-frp/reflex.git", toTextIgnore dir, "--branch", "master"]
279+ runOb_ ["thunk" , "pack", toTextIgnore dir]
280+ startingContents <- checkDir dir
281+ void $ errExit False $ runOb ["thunk", "update", toTextIgnore dir, "--branch", "dumble-palooza"]
282+ checkDir dir >>= liftIO . assertEqual "" startingContents
283+
284+ describe "ob hoogle" $ {- NOT parallel -} do
285+ it "starts a hoogle server on the given port" $ inTmpObInit $ \_ -> do
286+ [p0] <- liftIO $ getFreePorts 1
287+ maskExitSuccess $ runHandle "ob" ["hoogle", "--port", T.pack (show p0)] $ \stdout -> flip fix Nothing $ \loop -> \case
288+ Nothing -> do -- Still waiting for initial signal that the server has started
289+ ln <- liftIO $ T.hGetLine stdout
290+ let search = "Server starting on port " <> T.pack (show p0)
291+ case search `T.isInfixOf` ln of
292+ False -> loop Nothing -- keep waiting
293+ True -> loop $ Just 10
294+ Just (n :: Int) -> do -- Server has started and we have n attempts left
295+ let req uri = liftIO $ try @HTTP.HttpException $ HTTP.parseRequest uri >>= flip HTTP.httpLbs httpManager
296+ req ("http://127.0.0.1:" <> show p0) >>= \case
297+ Right r | HTTP.responseStatus r == HTTP.ok200 -> exit 0
298+ e -> if n <= 0
299+ then errorExit $ "Request to hoogle server failed: " <> T.pack (show e)
300+ else liftIO (threadDelay (1*10^(6 :: Int))) *> loop (Just $ n - 1)
301+ where
302+ verbosity = bool silently verbosely isVerbose
303+ nixBuild args = run nixBuildPath ("--no-out-link" : args)
304+
305+ runOb_ = augmentWithVerbosity run_ ob isVerbose
306+ runOb = augmentWithVerbosity run ob isVerbose
307+ testObRunInDir' = augmentWithVerbosity testObRunInDir ob isVerbose ["run"]
308+ testThunkPack' = augmentWithVerbosity testThunkPack ob isVerbose []
309+
310+ withObeliskImplClean f =
311+ withSystemTempDirectory "obelisk-impl-clean" $ \obeliskImpl -> do
312+ void . shellyOb verbosity $ chdir obeliskImpl $ do
313+ dirtyFiles <- T.strip <$> run gitPath ["-C", toTextIgnore obeliskRepoReadOnly, "diff", "--stat"]
314+ () <- when (dirtyFiles /= "") $ error "SelfTest does not work correctly with dirty obelisk repos as remote"
315+ run_ gitPath ["clone", "file://" <> toTextIgnore obeliskRepoReadOnly, toTextIgnore obeliskImpl]
316+ f obeliskImpl
317+
318+ withInitCache f =
319+ withSystemTempDirectory "init Cache λ" $ \initCache -> do
320+ -- Setup the ob init cache
321+ void . shellyOb verbosity $ chdir initCache $ do
322+ runOb_ ["init", "--symlink", toTextIgnore obeliskRepoReadOnly]
323+ run_ gitPath ["init"]
324+
325+ f initCache
326+
327+ shelly_ = void . shellyOb verbosity
328+
329+ defaultTmpDirName = "test λ"
330+
331+ inTmp' :: FilePath -> (FilePath -> Sh a) -> IO ()
332+ inTmp' dirname f = withTmp' dirname (chdir <*> f)
333+ inTmp = inTmp' defaultTmpDirName
334+
335+ withTmp' dirname f = shelly_ . withSystemTempDirectory dirname $ f . fromString
336+ withTmp = withTmp' defaultTmpDirName
337+
338+ assertRevEQ a b = liftIO . assertEqual "" "" =<< diff a b
339+ assertRevNE a b = liftIO . assertBool "" . (/= "") =<< diff a b
340+
341+ revParseHead = T.strip <$> run gitPath ["rev-parse", "HEAD"]
342+
343+ commitAll = do
344+ run_ gitPath ["add", "."]
345+ commit "checkpoint"
346+ revParseHead
347+
348+ thunk = ".obelisk/impl"
349+ update = runOb ["thunk", "update", thunk] *> commitAll
350+ pack = runOb ["thunk", "pack", thunk] *> commitAll
351+ unpack = runOb ["thunk", "unpack", thunk] *> commitAll
352+
353+ diff a b = run gitPath ["diff", a, b]
354+
355+
356+maskExitSuccess :: Sh () -> Sh ()
357+maskExitSuccess = handle_sh (\case ExitSuccess -> pure (); e -> throw e)
232
358
233-- | Run `ob run` in the given directory (maximum of one level deep)
359-- | Run `ob run` in the given directory (maximum of one level deep)
234−testObRunInDir :: Socket.PortNumber -> Socket.PortNumber -> Maybe Shelly.FilePath -> HTTP.Manager -> Sh ()
360+testObRunInDir :: String -> [Text] -> Maybe FilePath -> HTTP.Manager -> Sh ()
235−testObRunInDir p0 p1 mdir httpManager = handle_sh (\case ExitSuccess -> pure (); e -> throw e) $ do
361+testObRunInDir executable extraArgs mdir httpManager = maskExitSuccess $ do
362+ [p0, p1] <- liftIO $ getFreePorts 2
236 let uri p = "http://localhost:" <> T.pack (show p) <> "/" -- trailing slash required for comparison
363 let uri p = "http://localhost:" <> T.pack (show p) <> "/" -- trailing slash required for comparison
237 writefile "config/common/route" $ uri p0
364 writefile "config/common/route" $ uri p0
238− maybe id chdir mdir $ runHandle "ob" ["run"] $ \stdout -> do
365+ maybe id chdir mdir $ runHandles executable extraArgs [] $ \_stdin stdout stderr -> do
239− firstUri <- handleObRunStdout httpManager stdout
366+ firstUri <- handleObRunStdout httpManager stdout stderr
240 let newUri = uri p1
367 let newUri = uri p1
241 when (firstUri == newUri) $ errorExit $
368 when (firstUri == newUri) $ errorExit $
242 "Startup URI (" <> firstUri <> ") is the same as test URI (" <> newUri <> ")"
369 "Startup URI (" <> firstUri <> ") is the same as test URI (" <> newUri <> ")"
243 maybe id (\_ -> chdir "..") mdir $ alterRouteTo newUri stdout
370 maybe id (\_ -> chdir "..") mdir $ alterRouteTo newUri stdout
244− runningUri <- handleObRunStdout httpManager stdout
371+ runningUri <- handleObRunStdout httpManager stdout stderr
245 if runningUri /= newUri
372 if runningUri /= newUri
246 then errorExit $ "Reloading failed: expected " <> newUri <> " but got " <> runningUri
373 then errorExit $ "Reloading failed: expected " <> newUri <> " but got " <> runningUri
247 else exit 0
374 else exit 0
248
375
249−testThunkPack :: Shelly.FilePath -> Sh ()
376+testThunkPack :: String -> [Text] -> FilePath -> Sh ()
250−testThunkPack path' = withTempFile (T.unpack $ toTextIgnore path') "test-file" $ \file handle -> do
377+testThunkPack executable args path' = withTempFile (T.unpack $ toTextIgnore path') "test-file" $ \file handle -> do
251− let pack' = readProcessWithExitCode "ob" ["thunk", "pack", T.unpack $ toTextIgnore path'] ""
378+ let pack' = readProcessWithExitCode executable (T.unpack <$> ["thunk", "pack", toTextIgnore path'] ++ args) ""
252 ensureThunkPackFails q = liftIO $ pack' >>= \case
379 ensureThunkPackFails q = liftIO $ pack' >>= \case
253 (code, out, err)
380 (code, out, err)
254− | code == ExitSuccess -> fail "ob thunk pack succeeded when it should have failed"
381+ | code == ExitSuccess -> fail $ "ob thunk pack succeeded when it should have failed with error '" <> show q <> "'"
255 | q `T.isInfixOf` T.pack (out <> err) -> pure ()
382 | q `T.isInfixOf` T.pack (out <> err) -> pure ()
256− | otherwise -> fail $ "ob thunk pack failed for an unexpected reason: " <> show out <> "\nstderr: " <> err
383+ | otherwise -> fail $ "ob thunk pack failed for an unexpected reason, expecting '" <> show q <> "', received: " <> show out <> "\nstderr: " <> err
257− git = chdir path' . run "git"
384+ git = chdir path' . run gitPath
258 -- Untracked files
385 -- Untracked files
259 ensureThunkPackFails "Untracked files"
386 ensureThunkPackFails "Untracked files"
260 void $ git ["add", T.pack file]
387 void $ git ["add", T.pack file]
@@ -267,7 +394,7 @@ testThunkPack path' = withTempFile (T.unpack $ toTextIgnore path') "test-file" $
267 liftIO $ T.hPutStrLn handle "test file" >> hClose handle
394 liftIO $ T.hPutStrLn handle "test file" >> hClose handle
268 ensureThunkPackFails "modified"
395 ensureThunkPackFails "modified"
269 -- Existing stashes
396 -- Existing stashes
270− void $ git ["stash"]
397+ void $ git $ gitUserConfig <> [ "stash" ]
271 ensureThunkPackFails "has stashes"
398 ensureThunkPackFails "has stashes"
272
399
273-- | Blocks until a non-empty line is available
400-- | Blocks until a non-empty line is available
@@ -288,9 +415,12 @@ alterRouteTo uri stdout = do
288 "Reloading failed: " <> T.pack (show t)
415 "Reloading failed: " <> T.pack (show t)
289
416
290-- | Handle stdout of `ob run`: check that the frontend and backend servers are started correctly
417-- | Handle stdout of `ob run`: check that the frontend and backend servers are started correctly
291−handleObRunStdout :: HTTP.Manager -> Handle -> Sh Text
418+handleObRunStdout :: HTTP.Manager -> Handle -> Handle -> Sh Text
292−handleObRunStdout httpManager stdout = flip fix (ObRunState_Init, []) $ \loop (state, msgs) -> do
419+handleObRunStdout httpManager stdout stderr = flip fix (ObRunState_Init, []) $ \loop (state, msgs) -> do
293− liftIO (T.hGetLine stdout) >>= \t -> case state of
420+ isEOF <- liftIO $ hIsEOF stdout
421+ if isEOF
422+ then handleObRunError msgs
423+ else liftIO (T.hGetLine stdout) >>= \t -> case state of
294 ObRunState_Init
424 ObRunState_Init
295 | "Running test..." `T.isPrefixOf` t -> loop (ObRunState_BackendStarted, msgs)
425 | "Running test..." `T.isPrefixOf` t -> loop (ObRunState_BackendStarted, msgs)
296 ObRunState_Startup
426 ObRunState_Startup
@@ -302,8 +432,12 @@ handleObRunStdout httpManager stdout = flip fix (ObRunState_Init, []) $ \loop (s
302 obRunCheck httpManager stdout uri
432 obRunCheck httpManager stdout uri
303 pure uri
433 pure uri
304 | not (T.null t) -> errorExit $ "Started: " <> t -- If theres any other output here, startup failed
434 | not (T.null t) -> errorExit $ "Started: " <> t -- If theres any other output here, startup failed
305− _ | "Failed" `T.isPrefixOf` t -> errorExit $ "ob run failed: " <> T.unlines (reverse $ t : msgs)
435+ _ | "Failed" `T.isPrefixOf` t -> handleObRunError (t : msgs)
306 | otherwise -> loop (state, t : msgs)
436 | otherwise -> loop (state, t : msgs)
437+ where
438+ handleObRunError msgs = do
439+ stderrContent <- liftIO $ hGetContents stderr
440+ errorExit $ "ob run failed: " <> T.unlines (reverse msgs) <> " stderr: " <> T.pack stderrContent
307
441
308-- | Make requests to frontend/backend servers to check they are working properly
442-- | Make requests to frontend/backend servers to check they are working properly
309obRunCheck :: HTTP.Manager -> Handle -> Text -> Sh ()
443obRunCheck :: HTTP.Manager -> Handle -> Text -> Sh ()
lib/snap-extras/obelisk-snap-extras.cabal+1−1
@@ -1,6 +1,6 @@
1name: obelisk-snap-extras
1name: obelisk-snap-extras
2version: 0.1
2version: 0.1
3−synopsys: Extra functionality for Snap that should be considered for upstreaming
3+synopsis: Extra functionality for Snap that should be considered for upstreaming
4license: BSD3
4license: BSD3
5build-type: Simple
5build-type: Simple
6cabal-version: >=1.2
6cabal-version: >=1.2
lib/snap-extras/src/Obelisk/Snap/Extras.hs+1−1
@@ -2,13 +2,13 @@
2module Obelisk.Snap.Extras
2module Obelisk.Snap.Extras
3 ( cachePermanently
3 ( cachePermanently
4 , doNotCache
4 , doNotCache
5+ , ensureSecure
5 , serveFileIfExists
6 , serveFileIfExists
6 , serveFileIfExistsAs
7 , serveFileIfExistsAs
7 ) where
8 ) where
8
9
9import Control.Monad.IO.Class
10import Control.Monad.IO.Class
10import Data.ByteString (ByteString)
11import Data.ByteString (ByteString)
11−import Data.Monoid
12import Data.String
12import Data.String
13import Snap.Core
13import Snap.Core
14import Snap.Util.FileServe
14import Snap.Util.FileServe
lib/tabulation/CHANGELOG.mdnew file+5
@@ -0,0 +1,5 @@
1+# Revision history for tabulation
2+
3+## 0.1.0.0 -- 2019-05-24
4+
5+* First version. Basically carve out this library as a place for the HasFields class.
lib/tabulation/LICENSEnew file+30
@@ -0,0 +1,30 @@
1+Copyright (c) 2019, Cale Gibbard
2+
3+All rights reserved.
4+
5+Redistribution and use in source and binary forms, with or without
6+modification, are permitted provided that the following conditions are met:
7+
8+ * Redistributions of source code must retain the above copyright
9+ notice, this list of conditions and the following disclaimer.
10+
11+ * Redistributions in binary form must reproduce the above
12+ copyright notice, this list of conditions and the following
13+ disclaimer in the documentation and/or other materials provided
14+ with the distribution.
15+
16+ * Neither the name of Cale Gibbard nor the names of other
17+ contributors may be used to endorse or promote products derived
18+ from this software without specific prior written permission.
19+
20+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
lib/tabulation/Setup.hsnew file+2
@@ -0,0 +1,2 @@
1+import Distribution.Simple
2+main = defaultMain
lib/tabulation/src/Data/Tabulation.hsnew file+21
@@ -0,0 +1,21 @@
1+{-# LANGUAGE TypeFamilies #-}
2+{-# LANGUAGE RankNTypes #-}
3+
4+module Data.Tabulation where
5+
6+import Control.Lens
7+
8+-- | 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.
9+class HasFields a where
10+ type Field a :: * -> *
11+
12+ fieldLens :: Field a x -> Lens' a x
13+ tabulateFieldsA :: Applicative f => (forall x. Field a x -> f x) -> f a
14+
15+ tabulateFields :: (forall x. Field a x -> x) -> a
16+ tabulateFields f = runIdentity (tabulateFieldsA (Identity . f))
17+ traverseWithField :: Applicative m => (forall x. Field a x -> x -> m x) -> a -> m a
18+ traverseWithField t r = tabulateFieldsA (\f -> t f (indexField r f))
19+ indexField :: a -> Field a x -> x
20+ indexField a f = a ^. fieldLens f
21+ {-# MINIMAL fieldLens, tabulateFieldsA #-}
lib/tabulation/tabulation.cabalnew file+26
@@ -0,0 +1,26 @@
1+cabal-version: >=1.10
2+-- Initial package description 'tabulation.cabal' generated by 'cabal
3+-- init'. For further documentation, see
4+-- http://haskell.org/cabal/users-guide/
5+
6+name: tabulation
7+version: 0.1.0.0
8+synopsis: A library for types that can be completely reconstructed by fields as specified by a GADT.
9+-- description:
10+-- bug-reports:
11+license: BSD3
12+license-file: LICENSE
13+author: Cale Gibbard
14+maintainer: cale.gibbard@obsidian.systems
15+-- copyright:
16+category: Data
17+build-type: Simple
18+extra-source-files: CHANGELOG.md
19+
20+library
21+ exposed-modules: Data.Tabulation
22+ -- other-modules:
23+ -- other-extensions:
24+ build-depends: base, lens
25+ hs-source-dirs: src
26+ default-language: Haskell2010
nixpkgs-overlays/default.nix+11−6
@@ -1,10 +1,15 @@
1self: super:
1self: super:
2
2
3−{
3+let
4− cleanHaskellSource = builtins.filterSource (name: _: let baseName = builtins.baseNameOf name; in !(
4+ inherit (self) lib;
5− builtins.match "^\\.ghc\\.environment.*" baseName != null ||
5+ inherit (import ../dep/gitignore.nix { inherit lib; }) gitignoreSource;
6− baseName == "cabal.project.local"
6+in {
7− ));
7+ obeliskCleanSource = src:
8+ # WARNING: The order of application here seems to matter a great deal to
9+ # how quickly `ghcid` is able to reload changes. As a rule of thumb,
10+ # always apply `gitignoreSource` first.
11+ # See https://github.com/obsidiansystems/obelisk/pull/666 and related.
12+ lib.cleanSource (gitignoreSource src);
8
13
9− obeliskExecutableConfig = self.callPackage ../lib/executable-config { };
14+ obeliskExecutableConfig = self.callPackage ../lib/executable-config {};
10}
15}
release.nix+8−78
@@ -1,80 +1,10 @@
1−{ local-self ? import ./. {}
1+{
2+ supportedSystems ? [ "x86_64-linux" "x86_64-darwin" ]
2}:
3}:
4+rec {
5+ recurseForDerivations = true;
6+ build = import ./all-builds.nix { inherit supportedSystems; };
7+ test = import ./all-tests.nix { inherit supportedSystems; };
3
8
4−let
9+ inherit (build) metaCache;
5− inherit (local-self.nixpkgs) lib runCommand nix;
10+}
6−
7− cacheBuildSystems = [ "x86_64-linux" "x86_64-darwin" ];
8−
9− obeliskPackagesCommon = [
10− "obelisk-frontend"
11− "obelisk-route"
12− "obelisk-executable-config"
13− ];
14−
15− obeliskPackagesBackend = obeliskPackagesCommon ++ [
16− "obelisk-asset-manifest"
17− "obelisk-asset-serve-snap"
18− "obelisk-backend"
19− "obelisk-cliapp"
20− "obelisk-command"
21− "obelisk-executable-config-inject"
22− "obelisk-frontend"
23− "obelisk-run"
24− "obelisk-route"
25− "obelisk-selftest"
26− "obelisk-snap-extras"
27− ];
28−
29− pnameToAttrs = pkgsSet: pnames:
30− lib.listToAttrs (map
31− (name: { inherit name; value = pkgsSet.${name}; })
32− pnames);
33−
34− concatDepends = let
35− extractDeps = x: (x.override {
36− mkDerivation = drv: {
37− out = builtins.concatLists [
38− (drv.buildDepends or [])
39− (drv.libraryHaskellDepends or [])
40− (drv.executableHaskellDepends or [])
41− ];
42− };
43− }).out;
44− in pkgAttrs: builtins.concatLists (map extractDeps (builtins.attrValues pkgAttrs));
45−
46− perPlatform = lib.genAttrs cacheBuildSystems (system: let
47− obelisk = import ./. { inherit system; };
48− reflex-platform = obelisk.reflex-platform;
49− ghc = pnameToAttrs
50− obelisk.haskellPackageSets.ghc
51− obeliskPackagesBackend;
52− ghcjs = pnameToAttrs
53− obelisk.haskellPackageSets.ghcjs
54− obeliskPackagesCommon;
55− cachePackages = builtins.concatLists [
56− (builtins.attrValues ghc)
57− (builtins.attrValues ghcjs)
58− (concatDepends ghc)
59− (concatDepends ghcjs)
60− ];
61− command = local-self.command;
62− serverExeSkeleton = (import ./skeleton {}).exe;
63− builtSkeleton = (import ./skeleton {}).all;
64− in {
65− inherit
66− command
67− ghc ghcjs
68− serverExeSkeleton builtSkeleton;
69− cache = reflex-platform.pinBuildInputs
70− "obelisk-${system}"
71− cachePackages
72− [command serverExeSkeleton builtSkeleton];
73− });
74−
75− metaCache = local-self.pinBuildInputs
76− "obelisk-everywhere"
77− (map (a: a.cache) (builtins.attrValues perPlatform))
78− [];
79−
80−in perPlatform // { inherit metaCache; }
scripts/ghcid-this-lib+1−1
@@ -1,4 +1,4 @@
1#!/usr/bin/env bash
1#!/usr/bin/env bash
2set -euo pipefail
2set -euo pipefail
3
3
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+2
@@ -1,7 +1,9 @@
1dist-newstyle
1dist-newstyle
2result
2result
3+result-*
3result-android
4result-android
4result-ios
5result-ios
5result-exe
6result-exe
6.attr-cache
7.attr-cache
7ghcid-output.txt
8ghcid-output.txt
9+profile/
skeleton/backend/src/Backend.hs+1−6
@@ -1,8 +1,3 @@
1−{-# LANGUAGE EmptyCase #-}
2−{-# LANGUAGE FlexibleContexts #-}
3−{-# LANGUAGE LambdaCase #-}
4−{-# LANGUAGE RankNTypes #-}
5−{-# LANGUAGE TypeFamilies #-}
6module Backend where
1module Backend where
7
2
8import Common.Route
3import Common.Route
@@ -11,5 +6,5 @@ import Obelisk.Backend
11backend :: Backend BackendRoute FrontendRoute
6backend :: Backend BackendRoute FrontendRoute
12backend = Backend
7backend = Backend
13 { _backend_run = \serve -> serve $ const $ return ()
8 { _backend_run = \serve -> serve $ const $ return ()
14− , _backend_routeEncoder = backendRouteEncoder
9+ , _backend_routeEncoder = fullRouteEncoder
15 }
10 }
skeleton/common/common.cabal−1
@@ -9,7 +9,6 @@ library
9 , obelisk-route
9 , obelisk-route
10 , mtl
10 , mtl
11 , text
11 , text
12−
13 exposed-modules:
12 exposed-modules:
14 Common.Api
13 Common.Api
15 Common.Route
14 Common.Route
skeleton/common/src/Common/Api.hs+1−1
@@ -1,4 +1,4 @@
1module Common.Api where
1module Common.Api where
2
2
3commonStuff :: String
3commonStuff :: String
4−commonStuff = "Here is a string defined in code common to the frontend and backend."
4+commonStuff = "Here is a string defined in Common.Api"
skeleton/common/src/Common/Route.hs+13−18
@@ -1,15 +1,14 @@
1{-# LANGUAGE EmptyCase #-}
1{-# LANGUAGE EmptyCase #-}
2{-# LANGUAGE FlexibleContexts #-}
2{-# LANGUAGE FlexibleContexts #-}
3+{-# LANGUAGE FlexibleInstances #-}
3{-# LANGUAGE GADTs #-}
4{-# LANGUAGE GADTs #-}
4−{-# LANGUAGE LambdaCase #-}
5−{-# LANGUAGE RankNTypes #-}
6−{-# LANGUAGE TemplateHaskell #-}
7{-# LANGUAGE KindSignatures #-}
5{-# LANGUAGE KindSignatures #-}
8−{-# LANGUAGE EmptyCase #-}
6+{-# LANGUAGE LambdaCase #-}
9{-# LANGUAGE MultiParamTypeClasses #-}
7{-# LANGUAGE MultiParamTypeClasses #-}
10−{-# LANGUAGE FlexibleInstances #-}
11{-# LANGUAGE OverloadedStrings #-}
8{-# LANGUAGE OverloadedStrings #-}
12−
9+{-# LANGUAGE RankNTypes #-}
10+{-# LANGUAGE TemplateHaskell #-}
11+{-# LANGUAGE TypeFamilies #-}
13module Common.Route where
12module Common.Route where
14
13
15{- -- You will probably want these imports for composing Encoders.
14{- -- You will probably want these imports for composing Encoders.
@@ -19,7 +18,6 @@ import Control.Category
19
18
20import Data.Text (Text)
19import Data.Text (Text)
21import Data.Functor.Identity
20import Data.Functor.Identity
22−import Data.Functor.Sum
23
21
24import Obelisk.Route
22import Obelisk.Route
25import Obelisk.Route.TH
23import Obelisk.Route.TH
@@ -34,17 +32,14 @@ data FrontendRoute :: * -> * where
34 FrontendRoute_Main :: FrontendRoute ()
32 FrontendRoute_Main :: FrontendRoute ()
35 -- This type is used to define frontend routes, i.e. ones for which the backend will serve the frontend.
33 -- This type is used to define frontend routes, i.e. ones for which the backend will serve the frontend.
36
34
37−backendRouteEncoder
35+fullRouteEncoder
38− :: Encoder (Either Text) Identity (R (Sum BackendRoute (ObeliskRoute FrontendRoute))) PageName
36+ :: Encoder (Either Text) Identity (R (FullRoute BackendRoute FrontendRoute)) PageName
39−backendRouteEncoder = handleEncoder (const (InL BackendRoute_Missing :/ ())) $
37+fullRouteEncoder = mkFullRouteEncoder
40− pathComponentEncoder $ \case
38+ (FullRoute_Backend BackendRoute_Missing :/ ())
41− InL backendRoute -> case backendRoute of
39+ (\case
42− BackendRoute_Missing -> PathSegment "missing" $ unitEncoder mempty
40+ BackendRoute_Missing -> PathSegment "missing" $ unitEncoder mempty)
43− InR obeliskRoute -> obeliskRouteSegment obeliskRoute $ \case
41+ (\case
44− -- The encoder given to PathEnd determines how to parse query parameters,
42+ FrontendRoute_Main -> PathEnd $ unitEncoder mempty)
45− -- in this example, we have none, so we insist on it.
46− FrontendRoute_Main -> PathEnd $ unitEncoder mempty
47−
48
43
49concat <$> mapM deriveRouteComponent
44concat <$> mapM deriveRouteComponent
50 [ ''BackendRoute
45 [ ''BackendRoute
skeleton/config/common/examplenew file+1
@@ -0,0 +1 @@
1+This string comes from config/common/example
skeleton/default.nix+9−3
@@ -1,7 +1,13 @@
1−{ system ? builtins.currentSystem # TODO: Get rid of this system cruft
1+{ obelisk ? import ./.obelisk/impl {
2−, iosSdkVersion ? "10.2"
2+ system = builtins.currentSystem;
3+ iosSdkVersion = "10.2";
4+ # You must accept the Android Software Development Kit License Agreement at
5+ # https://developer.android.com/studio/terms in order to build Android apps.
6+ # Uncomment and set this to `true` to indicate your acceptance:
7+ # config.android_sdk.accept_license = false;
8+ }
3}:
9}:
4−with import ./.obelisk/impl { inherit system iosSdkVersion; };
10+with obelisk;
5project ./. ({ ... }: {
11project ./. ({ ... }: {
6 android.applicationId = "systems.obsidian.obelisk.examples.minimal";
12 android.applicationId = "systems.obsidian.obelisk.examples.minimal";
7 android.displayName = "Obelisk Minimal Example";
13 android.displayName = "Obelisk Minimal Example";
skeleton/frontend/frontend.cabal+2
@@ -9,7 +9,9 @@ library
9 , common
9 , common
10 , obelisk-frontend
10 , obelisk-frontend
11 , obelisk-route
11 , obelisk-route
12+ , jsaddle
12 , reflex-dom
13 , reflex-dom
14+ , obelisk-executable-config-lookup
13 , obelisk-generated-static
15 , obelisk-generated-static
14 , text
16 , text
15 exposed-modules:
17 exposed-modules:
skeleton/frontend/src-bin/main.hs+1−1
@@ -6,5 +6,5 @@ import Reflex.Dom
6
6
7main :: IO ()
7main :: IO ()
8main = do
8main = do
9− let Right validFullEncoder = checkEncoder backendRouteEncoder
9+ let Right validFullEncoder = checkEncoder fullRouteEncoder
10 run $ runFrontend validFullEncoder frontend
10 run $ runFrontend validFullEncoder frontend
skeleton/frontend/src/Frontend.hs+28−3
@@ -1,23 +1,48 @@
1{-# LANGUAGE DataKinds #-}
1{-# LANGUAGE DataKinds #-}
2{-# LANGUAGE OverloadedStrings #-}
2{-# LANGUAGE OverloadedStrings #-}
3{-# LANGUAGE TypeApplications #-}
3{-# LANGUAGE TypeApplications #-}
4+
4module Frontend where
5module Frontend where
5
6
7+import Control.Monad
6import qualified Data.Text as T
8import qualified Data.Text as T
9+import qualified Data.Text.Encoding as T
10+import Language.Javascript.JSaddle (eval, liftJSM)
11+
7import Obelisk.Frontend
12import Obelisk.Frontend
13+import Obelisk.Configs
8import Obelisk.Route
14import Obelisk.Route
15+import Obelisk.Generated.Static
16+
9import Reflex.Dom.Core
17import Reflex.Dom.Core
10
18
11import Common.Api
19import Common.Api
12import Common.Route
20import Common.Route
13−import Obelisk.Generated.Static
14
21
15
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.
16frontend :: Frontend (R FrontendRoute)
26frontend :: Frontend (R FrontendRoute)
17frontend = Frontend
27frontend = Frontend
18− { _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
19 , _frontend_body = do
31 , _frontend_body = do
20− text "Welcome to Obelisk!"
32+ el "h1" $ text "Welcome to Obelisk!"
21 el "p" $ text $ T.pack commonStuff
33 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+
22 elAttr "img" ("src" =: static @"obelisk.jpg") blank
41 elAttr "img" ("src" =: static @"obelisk.jpg") blank
42+ el "div" $ do
43+ exampleConfig <- getConfig "common/example"
44+ case exampleConfig of
45+ Nothing -> text "No config file found in config/common/example"
46+ Just s -> text $ T.decodeUtf8 s
47+ return ()
23 }
48 }
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+}