on master help sign in
Merge remote-tracking branch 'origin/develop' into expose-overlay
commit2e3d492c4375735570adf45ff1d676040a1d9da1 authorJohn Ericson <John.Ericson@Obsidian.Systems> dateMon 30 Mar 2020, 17:30 (7 years ago) parent0f502dcf5cf750
118 files changed+4939−1889
.github/pull_request_template.md+8 .gitignore+2−3 .hlint.yaml+65 CONTRIBUTING.md+73−3 ChangeLog.md+111 FAQ.md+107 LICENSE+30 README.md+144−27 all-builds.nix+104 all-tests.nix+145 default.nix+189−108 dep/.gitignore+1 dep/ghcid/default.nix+7 dep/ghcid/github.json+7 dep/gitignore.nix/default.nix+8 dep/gitignore.nix/github.json+8 dep/hnix/default.nix+7 dep/hnix/github.json+7 dep/reflex-platform/default.nix+7−6 dep/reflex-platform/github.json+5−4 dep/snap-core/default.nix+8 dep/snap-core/github.json+8 guides/app-deploy/README.md+311 guides/app-deploy/assets/android-app.jpg guides/app-deploy/assets/android-confirm-usb-debugging.jpg guides/app-deploy/assets/app-deploy-2.png guides/app-deploy/assets/app-deploy.png guides/app-deploy/assets/ios-deploy-example.png guides/app-deploy/assets/ios-deploy-keychain.png guides/app-deploy/assets/ios-obelisk-app-icon.jpg guides/app-deploy/assets/ios-obelisk-app-open.jpg guides/app-deploy/assets/nano-edit-nixos-configuration.png guides/app-deploy/assets/virtualbox-appliance-import-loading.png guides/app-deploy/assets/virtualbox-appliance-import.png guides/app-deploy/assets/virtualbox-dashboard-nixos.png guides/app-deploy/assets/virtualbox-image-settings-network.png guides/app-deploy/assets/virtualbox-nioxs-after-boot.png guides/app-deploy/assets/xcode-devices.png haskell-overlays/misc-deps.nix+11−14 haskell-overlays/obelisk.nix+31−12 haskell-overlays/tighten-ob-exes.nix+11−30 jobsets.nix−66 lib/asset/README.md+10−10 lib/asset/manifest/obelisk-asset-manifest.cabal+1 lib/asset/manifest/src/Obelisk/Asset/Gather.hs+5−2 lib/asset/serve-snap/src/Obelisk/Asset/Accept.hs+6−6 lib/asset/serve-snap/src/Obelisk/Asset/Serve/Snap.hs+21−10 lib/backend/obelisk-backend.cabal+3 lib/backend/src/Obelisk/Backend.hs+180−57 lib/cliapp/obelisk-cliapp.cabal+4−2 lib/cliapp/src/Obelisk/CliApp.hs+15−6 lib/cliapp/src/Obelisk/CliApp/Demo.hs−52 lib/cliapp/src/Obelisk/CliApp/Logging.hs+35−10 lib/cliapp/src/Obelisk/CliApp/Process.hs+112−43 lib/cliapp/src/Obelisk/CliApp/Spinner.hs+18−23 lib/cliapp/src/Obelisk/CliApp/Theme.hs+29 lib/cliapp/src/Obelisk/CliApp/Types.hs+10−3 lib/command/obelisk-command.cabal+11−12 lib/command/src/Obelisk/App.hs+5−2 lib/command/src/Obelisk/Command.hs+122−86 lib/command/src/Obelisk/Command/Deploy.hs+89−83 lib/command/src/Obelisk/Command/Nix.hs+65−25 lib/command/src/Obelisk/Command/Preprocessor.hs+96 lib/command/src/Obelisk/Command/Project.hs+164−54 lib/command/src/Obelisk/Command/Run.hs+307−121 lib/command/src/Obelisk/Command/Thunk.hs+259−127 lib/command/src/Obelisk/Command/Upgrade/Hash.hs−114 lib/command/src/Obelisk/Command/Utils.hs+43−18 lib/command/src/Obelisk/Command/VmBuilder.hs+2−2 lib/executable-config/default.nix+6−3 lib/executable-config/inject/obelisk-executable-config-inject.cabal+2−2 lib/executable-config/inject/src/Obelisk/ExecutableConfig/Inject.hs+14−40 lib/executable-config/lookup/default.nix−24 lib/executable-config/lookup/obelisk-executable-config-lookup.cabal+45 lib/executable-config/lookup/obelisk-executable-config.cabal−30 lib/executable-config/lookup/src-android/Obelisk/ExecutableConfig.hs−27 lib/executable-config/lookup/src-android/Obelisk/ExecutableConfig/Lookup.hs+60 lib/executable-config/lookup/src-ghcjs/Obelisk/ExecutableConfig.hs−17 lib/executable-config/lookup/src-ghcjs/Obelisk/ExecutableConfig/Lookup.hs+51 lib/executable-config/lookup/src-ios/Obelisk/ExecutableConfig.hs−22 lib/executable-config/lookup/src-ios/Obelisk/ExecutableConfig/Lookup.hs+17 lib/executable-config/lookup/src-other/Obelisk/ExecutableConfig/Lookup.hs+11 lib/executable-config/lookup/src/Obelisk/Configs.hs+129 lib/executable-config/lookup/src/Obelisk/Configs/Internal/Directory.hs+24 lib/executable-config/lookup/src/Obelisk/ExecutableConfig.hs−13 lib/frontend/obelisk-frontend.cabal+8−1 lib/frontend/src/Obelisk/Frontend.hs+182−96 lib/frontend/src/Obelisk/Frontend/Cookie.hs+103 lib/route/obelisk-route.cabal+7−2 lib/route/src/Obelisk/Route.hs+395−40 lib/route/src/Obelisk/Route/Frontend.hs+226−51 lib/route/src/Obelisk/Route/TH.hs+4−5 lib/run/obelisk-run.cabal+10−2 lib/run/src/Obelisk/Run.hs+120−59 lib/selftest/obelisk-selftest.cabal+5−3 lib/selftest/src/Obelisk/SelfTest.hs+325−191 lib/snap-extras/obelisk-snap-extras.cabal+1−1 lib/snap-extras/src/Obelisk/Snap/Extras.hs+1−1 lib/tabulation/CHANGELOG.md+5 lib/tabulation/LICENSE+30 lib/tabulation/Setup.hs+2 lib/tabulation/src/Data/Tabulation.hs+21 lib/tabulation/tabulation.cabal+26 nixpkgs-overlays/default.nix+11−6 release.nix+8−78 scripts/ghcid-this-lib+1−1 skeleton/.gitignore+2 skeleton/backend/src/Backend.hs+1−6 skeleton/common/common.cabal−1 skeleton/common/src/Common/Api.hs+1−1 skeleton/common/src/Common/Route.hs+13−18 skeleton/config/common/example+1 skeleton/default.nix+9−3 skeleton/frontend/frontend.cabal+2 skeleton/frontend/src-bin/main.hs+1−1 skeleton/frontend/src/Frontend.hs+28−3 skeleton/shell.nix+1 skeleton/static/main.css+3
.github/pull_request_template.mdnew file+8
@@ -0,0 +1,8 @@
1+<!-- Provide a clear overview of your changes. -->
2+
3+I have:
4+
5+ - [ ] Based work on latest `develop` branch
6+ - [ ] Looked for lint in my changes with `hlint .` (lint found code you did not write can be left alone)
7+ - [ ] Run the test suite: `$(nix-build -A selftest --no-out-link)`
8+ - [ ] (Optional) Run CI tests locally: `nix-build release.nix -A build.x86_64-linux --no-out-link` (or `x86_64-darwin` on macOS)
.gitignore+2−3
@@ -2,14 +2,13 @@
22**/.cabal-sandbox
33**/cabal.sandbox.config
44result
5+result-*
56*.o
67*.hi
7−
88TAGS
99tags
1010ctags
1111dist-newstyle
1212cabal.project.local
1313.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
22
3−Thanks for taking the time to contribute!
3+Contributions and issue reports are encouraged and appreciated!
44
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)
611
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:
753```bash
854$(nix-build -A selftest --no-out-link)
955```
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 @@
11# Frequently Asked Questions
22
331. [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)
47
58### How do I fix invalid entitlements?
69
@@ -13,3 +16,107 @@ When this happens you'll see an error something like this:
1316
1417Fixing the value of `ios.bundleIdentifier` should fix the error.
1518
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 @@
11# Obelisk
22
3+[![Haskell Programming Language](https://img.shields.io/badge/language-Haskell-blue.svg)](http://www.haskell.org)
4+[![BSD3 License](http://img.shields.io/badge/license-BSD3-brightgreen.svg)](https://tldrlegal.com/license/bsd-3-clause-license-%28revised%29)
5+
6+
37Obelisk provides an easy way to develop and deploy your [Reflex](https://github.com/reflex-frp/reflex) project as web apps and as mobile apps.
48
59- [Installing Obelisk](#installing-obelisk)
610- [Developing an Obelisk project](#developing-an-obelisk-project)
11+ - [Adding Packages](#adding-packages)
12+ - [Adding Package Overrides](#adding-package-overrides)
13+ - [Running over https](#running-over-https)
714- [Deploying](#deploying)
815 - [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)
1019 - [From macOS](#from-macos)
1120 - [Deploying an updated version](#deploying-an-updated-version)
1221- [Mobile](#mobile)
@@ -19,13 +28,15 @@ Obelisk provides an easy way to develop and deploy your [Reflex](https://github.
19281. Set up nix caches
2029 1. If you are running NixOS, add this to `/etc/nixos/configuration.nix`:
2130 ```
22− nix.binaryCaches = [ "https://cache.nixos.org/" "https://nixcache.reflex-frp.org" ];
31+ nix.binaryCaches = [ "https://nixcache.reflex-frp.org" ];
2332 nix.binaryCachePublicKeys = [ "ryantrinkle.com-1:JJiAKaRv9mWgpVAz8dwewnZe0AzzEAzPkagE9SP5NWI=" ];
2433 ```
34+ and rebuild your NixOS configuration (e.g. `sudo nixos-rebuild switch`).
2535 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)):
2636 ```
27− substituters = https://cache.nixos.org https://nixcache.reflex-frp.org
28− trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= ryantrinkle.com-1:JJiAKaRv9mWgpVAz8dwewnZe0AzzEAzPkagE9SP5NWI=
37+ binary-caches = https://cache.nixos.org https://nixcache.reflex-frp.org
38+ binary-cache-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= ryantrinkle.com-1:JJiAKaRv9mWgpVAz8dwewnZe0AzzEAzPkagE9SP5NWI=
39+ binary-caches-parallel-connections = 40
2940 ```
3041 * 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)
3142 ```
@@ -46,16 +57,38 @@ Obelisk provides an easy way to develop and deploy your [Reflex](https://github.
4657
4758When developing on obelisk itself you may launch `ghcid` for the corresponding project as follows. For example to launch ghcid for `lib/backend` project:
4859
49−```
60+```bash
5061nix-shell -A obeliskEnvs.obelisk-backend --run "cd lib/backend && ghcid -c 'cabal new-repl'"
5162```
5263
5364Or to launch ghcid for `lib/command` project:
5465
55−```
66+```bash
5667nix-shell -A obeliskEnvs.obelisk-command --run "cd lib/command && ghcid -c 'cabal new-repl'"
5768```
5869
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+
5992### Accessing private repositories
6093
6194To allow the Nix builder to access private git repositories, you must be set up
@@ -69,44 +102,103 @@ access to:
69102
70103To create a new Obelisk project, go to an empty directory and run:
71104
72−```
105+```bash
73106ob init
74107```
75108
76109Obelisk 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:
77110
78−```
111+```bash
79112ob run
80113```
81114
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.
83117
84118Every 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.
85119
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+
86176## Deploying
87177
88178### Locally
89179
90180Build everything:
91181
92−```
93−nix-build -A exe -o result-exe
182+```bash
183+nix-build -A exe --no-out-link
94184```
95185
96−Run the server:
186+Copy the result to a new directory, add configuration, and run!
97187
98−```
99−cd result-exe
100−./backend
188+```bash
189+mkdir test-app
190+ln -s $(nix-build -A exe --no-out-link)/* test-app/
191+cp -r config test-app
192+(cd test-app && ./backend)
101193```
102194
103−### EC2
195+### Default EC2 Deployment
104196
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)).
106198
107199First create a new EC2 instance:
108200
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))
1102021. In the instance configuration wizard ensure that your instance has at least 1GB RAM and 10GB disk space.
1112031. When prompted save your AWS private key (`~/myaws.pem`) somewhere safe. We'll need it later during deployment.
1122041. 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 \
129221 ~/code/myapp-deploy
130222```
131223
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.
133227
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!
135229
136230```
137231cd ~/code/myapp-deploy
@@ -142,6 +236,26 @@ ob deploy push
142236
143237At 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`.
144238
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+
145259### From macOS
146260
147261Deploying from macOS requires some extra setup:
@@ -155,7 +269,7 @@ Running `ob deploy push` will give you additional setup instructions.
155269
156270If 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:
157271
158−```
272+```bash
159273cd ~/code/myapp-deploy
160274ob deploy update
161275ob deploy push
@@ -181,14 +295,14 @@ These versions will work out of the box but iOS SDKs prior to 11.3 should also w
181295
182296More recent Xcodes should also work, as long as one of the SDKs mentioned above has been used.
183297To 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
185299open -W Xcode_9.2.xip
186300sudo 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
187301```
188302
189303
190304You can verify that you have correct versions by running
191−```
305+```bash
192306xcodebuild -showsdks
193307```
194308
@@ -214,7 +328,7 @@ Ensure that `bundleIdentifier` matches the App ID of the development profile, or
2143281. Connect the registered iPhone.
2153291. Find your Apple Team ID in the [developer portal](https://developer.apple.com/account/#/membership).
2163301. Run the deploy command with your Team ID:
217−```
331+```bash
218332result-ios/bin/deploy [TEAM_ID]
219333# or in debug mode via lldb:
220334result-ios/bin/deploy [TEAM_ID] -d
@@ -224,7 +338,7 @@ result-ios/bin/deploy [TEAM_ID] -d
2243381. Go to [developer portal - distribution profiles](https://developer.apple.com/account/ios/profile/production).
225339Create and download a distribution profile.
2263401. Run the package script with your TEAM ID and your distribution profile to create a `.ipa`:
227−```
341+```bash
228342result-ios/bin/package [TEAM_ID] /path/to/output/.ipa /path/to/profile/file
229343```
230344
@@ -237,7 +351,10 @@ It's also possible to inspect iOS WkWebView apps once they are installed in the
237351
238352### Android
239353
354+NOTE: Currently Android builds are only supported on Linux.
355+
2403561. 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.
2413581. Run `nix-build -A android.frontend -o result-android` to build the Android app.
2423591. A debug version of the app should be generated at `result-android/android-app-debug.apk`
243360
@@ -254,9 +371,9 @@ This command will accomplish the following:
2543711. Build a Signed Android apk for your application
2553721. Deploy the Signed apk to your connected Android device
256373
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`.
258375
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).
260377
261378This 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`.
262379
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 @@
11{ system ? builtins.currentSystem
22, profiling ? false
33, iosSdkVersion ? "10.2"
4−, __useLegacyCompilers ? false
4+, config ? {}
5+, reflex-platform-func ? import ./dep/reflex-platform
56}:
67let
8+ reflex-platform = getReflexPlatform { inherit system; };
9+ inherit (reflex-platform) hackGet nixpkgs;
10+ pkgs = nixpkgs;
711
8− getReflexPlatform = getReflexPlatform' __useLegacyCompilers;
9− getReflexPlatform' = __useLegacyCompilers: sys: import ./dep/reflex-platform {
10− inherit iosSdkVersion __useLegacyCompilers;
11− system = sys;
12− enableLibraryProfiling = profiling;
12+ inherit (import dep/gitignore.nix { inherit (nixpkgs) lib; }) gitignoreSource;
13+
14+ getReflexPlatform = { system, enableLibraryProfiling ? profiling }: reflex-platform-func {
15+ inherit iosSdkVersion config system enableLibraryProfiling;
1316
1417 nixpkgsOverlays = [
1518 (import ./nixpkgs-overlays)
1619 ];
1720
1821 haskellOverlays = [
19− (import ./haskell-overlays/misc-deps.nix)
22+ (import ./haskell-overlays/misc-deps.nix { inherit hackGet; })
23+ pkgs.obeliskExecutableConfig.haskellOverlay
2024 (import ./haskell-overlays/obelisk.nix)
2125 (import ./haskell-overlays/tighten-ob-exes.nix)
2226 ];
2327 };
2428
25− reflex-platform = getReflexPlatform' false system;
26− inherit (reflex-platform) hackGet nixpkgs;
27− pkgs = nixpkgs;
28−
2929 # The haskell environment used to build Obelisk itself, e.g. the 'ob' command
3030 ghcObelisk = reflex-platform.ghc;
3131
@@ -43,19 +43,18 @@ in rec {
4343 pathGit = ./.; # Used in CI by the migration graph hash algorithm to correctly ignore files.
4444 path = reflex-platform.filterGit ./.;
4545 obelisk = ghcObelisk;
46− obeliskEnvs = ghcObeliskEnvs;
46+ obeliskEnvs = pkgs.lib.filterAttrs (k: _: pkgs.lib.strings.hasPrefix "obelisk-" k) ghcObeliskEnvs;
4747 command = ghcObelisk.obelisk-command;
48− shell = pinBuildInputs "obelisk-shell" ([command] ++ command.commandRuntimeDeps) [];
48+ shell = pinBuildInputs "obelisk-shell" ([command] ++ command.commandRuntimeDeps);
4949
5050 selftest = pkgs.writeScript "selftest" ''
51− #!/usr/bin/env bash
51+ #!${pkgs.runtimeShell}
5252 set -euo pipefail
5353
5454 PATH="${command}/bin:$PATH"
55− export OBELISK_IMPL="${hackGet ./.}"
55+ cd ${./.}
5656 "${ghcObelisk.obelisk-selftest}/bin/obelisk-selftest" +RTS -N -RTS "$@"
5757 '';
58− #TODO: Why can't I build ./skeleton directly as a derivation? `nix-build -E ./.` doesn't work
5958 skeleton = pkgs.runCommand "skeleton" {
6059 dir = builtins.filterSource (path: type: builtins.trace path (baseNameOf path != ".obelisk")) ./skeleton;
6160 } ''
@@ -74,27 +73,42 @@ in rec {
7473 obelisk-asset-manifest-generate "$src" "$haskellManifest" ${packageName} ${moduleName} "$symlinked"
7574 '';
7675
77− compressedJs = frontend: optimizationLevel: pkgs.runCommand "compressedJs" { buildInputs = [ pkgs.closurecompiler ]; } ''
76+ compressedJs = frontend: optimizationLevel: pkgs.runCommand "compressedJs" {} ''
7877 mkdir $out
7978 cd $out
79+ # TODO profiling + static shouldn't break and need an ad-hoc workaround like that
8080 ln -s "${haskellLib.justStaticExecutables frontend}/bin/frontend.jsexe/all.js" all.unminified.js
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
82− echo "//# sourceMappingURL=all.js.map" >> all.js
81+ ${if optimizationLevel == null then ''
82+ ln -s all.unminified.js all.js
83+ '' else ''
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
85+ echo "//# sourceMappingURL=all.js.map" >> all.js
86+ ''}
8387 '';
8488
8589 serverModules = {
86− mkBaseEc2 = { hostName, routeHost, enableHttps, adminEmail, ... }: {...}: {
90+ mkBaseEc2 = { nixosPkgs, ... }: {...}: {
8791 imports = [
88− (pkgs.path + /nixos/modules/virtualisation/amazon-image.nix)
92+ (nixosPkgs.path + /nixos/modules/virtualisation/amazon-image.nix)
8993 ];
94+ ec2.hvm = true;
95+ };
96+
97+ mkDefaultNetworking = { adminEmail, enableHttps, hostName, routeHost, ... }: {...}: {
9098 networking = {
9199 inherit hostName;
92100 firewall.allowedTCPPorts = if enableHttps then [ 80 443 ] else [ 80 ];
93101 };
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+
94109 security.acme.certs = if enableHttps then {
95110 "${routeHost}".email = adminEmail;
96111 } else {};
97− ec2.hvm = true;
98112 };
99113
100114 mkObeliskApp =
@@ -115,7 +129,8 @@ in rec {
115129 enableACME = enableHttps;
116130 forceSSL = enableHttps;
117131 locations.${baseUrl} = {
118− proxyPass = "http://localhost:" + toString internalPort;
132+ proxyPass = "http://127.0.0.1:" + toString internalPort;
133+ proxyWebsockets = true;
119134 };
120135 };
121136 };
@@ -123,10 +138,11 @@ in rec {
123138 wantedBy = [ "multi-user.target" ];
124139 after = [ "network.target" ];
125140 restartIfChanged = true;
141+ path = [ pkgs.gnutar ];
126142 script = ''
127143 ln -sft . '${exe}'/*
128144 mkdir -p log
129− exec ./backend ${backendArgs} >>backend.out 2>>backend.err </dev/null
145+ exec ./backend ${backendArgs} </dev/null
130146 '';
131147 serviceConfig = {
132148 User = user;
@@ -149,119 +165,184 @@ in rec {
149165 };
150166 };
151167
168+ inherit mkAssets;
169+
152170 serverExe = backend: frontend: assets: optimizationLevel: version:
153171 pkgs.runCommand "serverExe" {} ''
154172 mkdir $out
155173 set -eux
156− ln -s "${haskellLib.justStaticExecutables backend}"/bin/* $out/
174+ ln -s "${if profiling then backend else haskellLib.justStaticExecutables backend}"/bin/* $out/
157175 ln -s "${mkAssets assets}" $out/static.assets
158176 ln -s ${mkAssets (compressedJs frontend optimizationLevel)} $out/frontend.jsexe.assets
159177 echo ${version} > $out/version
160178 '';
161179
162− server = { exe, hostName, adminEmail, routeHost, enableHttps, version }@args:
180+ server = { exe, hostName, adminEmail, routeHost, enableHttps, version, module ? serverModules.mkBaseEc2 }@args:
163181 let
164182 nixos = import (pkgs.path + /nixos);
165183 in nixos {
166184 system = "x86_64-linux";
167185 configuration = {
168186 imports = [
169− (serverModules.mkBaseEc2 args)
187+ (module { inherit exe hostName adminEmail routeHost enableHttps version; nixosPkgs = pkgs; })
188+ (serverModules.mkDefaultNetworking args)
170189 (serverModules.mkObeliskApp args)
171190 ];
172191 };
173192 };
174193
175−
176194 # An Obelisk project is a reflex-platform project with a predefined layout and role for each component
177− project = base: projectDefinition:
178− let projectOut = sys: (getReflexPlatform sys).project (args@{ nixpkgs, ... }:
179− let mkProject = { android ? null #TODO: Better error when missing
180− , ios ? null #TODO: Better error when missing
181− , packages ? {}
182− , overrides ? _: _: {}
183− , staticFiles ? base + /static
184− , tools ? _: []
185− , shellToolOverrides ? _: _: {}
186− , withHoogle ? false # Setting this to `true` makes shell reloading far slower
187− , __closureCompilerOptimizationLevel ? "ADVANCED"
188− }:
189− let frontendName = "frontend";
190− backendName = "backend";
191− commonName = "common";
192− staticName = "obelisk-generated-static";
193− staticFilesImpure = if lib.isDerivation staticFiles then staticFiles else toString staticFiles;
194− processedStatic = processAssets { src = staticFiles; };
195− # The packages whose names and roles are defined by this package
196− predefinedPackages = lib.filterAttrs (_: x: x != null) {
197− ${frontendName} = nullIfAbsent (base + "/frontend");
198− ${commonName} = nullIfAbsent (base + "/common");
199− ${backendName} = nullIfAbsent (base + "/backend");
200− };
201− combinedPackages = predefinedPackages // packages;
202− projectOverrides = self: super: {
203− ${staticName} = haskellLib.dontHaddock (self.callCabal2nix staticName processedStatic.haskellManifest {});
204− ${backendName} = haskellLib.addBuildDepend super.${backendName} self.obelisk-run;
205− };
206− totalOverrides = lib.composeExtensions projectOverrides overrides;
207− inherit (lib.strings) hasPrefix;
208− privateConfigDirs = ["config/backend"];
209− injectableConfig = builtins.filterSource (path: _:
210− !(lib.lists.any (x: hasPrefix (toString base + "/" + toString x) (toString path)) privateConfigDirs)
211− );
212− __android = configPath: {
213− ${if android == null then null else frontendName} = {
214− executableName = "frontend";
215− ${if builtins.pathExists staticFiles then "assets" else null} =
216− nixpkgs.obeliskExecutableConfig.platforms.android.inject (injectableConfig configPath) processedStatic.symlinked;
217− } // android;
195+ project = base': projectDefinition:
196+ let
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 =
201+ { android ? null #TODO: Better error when missing
202+ , ios ? null #TODO: Better error when missing
203+ , packages ? {}
204+ , overrides ? _: _: {}
205+ , staticFiles ? null
206+ , tools ? _: []
207+ , shellToolOverrides ? _: _: {}
208+ , withHoogle ? false # Setting this to `true` makes shell reloading far slower
209+ , __closureCompilerOptimizationLevel ? "ADVANCED" # Set this to `null` to skip the closure-compiler step
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+ };
219+ frontendName = "frontend";
220+ backendName = "backend";
221+ commonName = "common";
222+ staticName = "obelisk-generated-static";
223+ staticFilesImpure = let fs = self.userSettings.staticFiles; in if lib.isDerivation fs then fs else toString fs;
224+ processedStatic = processAssets { src = self.userSettings.staticFiles; };
225+ # The packages whose names and roles are defined by this package
226+ predefinedPackages = lib.filterAttrs (_: x: x != null) {
227+ ${self.frontendName} = nullIfAbsent (self.base + "/frontend");
228+ ${self.commonName} = nullIfAbsent (self.base + "/common");
229+ ${self.backendName} = nullIfAbsent (self.base + "/backend");
230+ };
231+ shellPackages = {};
232+ combinedPackages = self.predefinedPackages // self.userSettings.packages // self.shellPackages;
233+ projectOverrides = self': super': {
234+ ${self.staticName} = haskellLib.dontHaddock (self'.callCabal2nix self.staticName self.processedStatic.haskellManifest {});
235+ ${self.backendName} = haskellLib.addBuildDepend super'.${self.backendName} self'.obelisk-run;
236+ };
237+ totalOverrides = lib.composeExtensions self.projectOverrides self.userSettings.overrides;
238+ privateConfigDirs = ["config/backend"];
239+ injectableConfig = builtins.filterSource (path: _:
240+ !(lib.lists.any (x: hasPrefix (toString self.base + "/" + toString x) (toString path)) self.privateConfigDirs)
241+ );
242+ __androidWithConfig = configPath: {
243+ ${if self.userSettings.android == null then null else self.frontendName} = {
244+ executableName = "frontend";
245+ ${if builtins.pathExists self.userSettings.staticFiles then "assets" else null} =
246+ nixpkgs.obeliskExecutableConfig.platforms.android.inject
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;
218283 };
219− __ios = configPath: {
220− ${if ios == null then null else frontendName} = {
221− executableName = "frontend";
222− ${if builtins.pathExists staticFiles then "staticSrc" else null} =
223− nixpkgs.obeliskExecutableConfig.platforms.ios.inject (injectableConfig configPath) processedStatic.symlinked;
224− } // ios;
284+ android = self.__androidWithConfig (self.base + "/config");
285+ ios = self.__iosWithConfig (self.base + "/config");
286+
287+ passthru = {
288+ __unstable__.self = allConfig;
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+ ;
225297 };
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− ];
244298 };
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− };
249− in mkProject (projectDefinition args));
250− serverOn = sys: version: serverExe
251− (projectOut sys).ghc.backend
252− (projectOut system).ghcjs.frontend
253− (projectOut sys).passthru.staticFiles
254− (projectOut sys).passthru.__closureCompilerOptimizationLevel
299+ });
300+ in allConfig;
301+ in (mkProject (projectDefinition args)).projectConfig);
302+ mainProjectOut = projectOut { inherit system; };
303+ serverOn = projectInst: version: serverExe
304+ projectInst.ghc.backend
305+ mainProjectOut.ghcjs.frontend
306+ projectInst.passthru.staticFiles
307+ projectInst.passthru.__closureCompilerOptimizationLevel
255308 version;
256− linuxExe = serverOn "x86_64-linux";
309+ linuxExe = serverOn (projectOut { system = "x86_64-linux"; });
257310 dummyVersion = "Version number is only available for deployments";
258− in projectOut system // {
311+ in mainProjectOut // {
312+ __unstable__.profiledObRun = let
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+
259340 linuxExeConfigurable = linuxExe;
260341 linuxExe = linuxExe dummyVersion;
261− exe = serverOn system dummyVersion;
262− server = args@{ hostName, adminEmail, routeHost, enableHttps, version }:
342+ exe = serverOn mainProjectOut dummyVersion;
343+ server = args@{ hostName, adminEmail, routeHost, enableHttps, version, module ? serverModules.mkBaseEc2 }:
263344 server (args // { exe = linuxExe version; });
264− obelisk = import (base + "/.obelisk/impl") {};
345+ obelisk = import (base' + "/.obelisk/impl") {};
265346 };
266347 haskellPackageSets = {
267348 inherit (reflex-platform) ghc ghcjs;
dep/.gitignore+1
@@ -1,4 +1,5 @@
11*
2+!*/
23!.gitignore
34!*/*.json
45!*/default.nix
dep/ghcid/default.nixnew file+7
@@ -0,0 +1,7 @@
1+# DO NOT HAND-EDIT THIS FILE
2+import ((import <nixpkgs> {}).fetchFromGitHub (
3+ let json = builtins.fromJSON (builtins.readFile ./github.json);
4+ in { inherit (json) owner repo rev sha256;
5+ private = json.private or false;
6+ }
7+))
dep/ghcid/github.jsonnew file+7
@@ -0,0 +1,7 @@
1+{
2+ "owner": "ndmitchell",
3+ "repo": "ghcid",
4+ "branch": "master",
5+ "rev": "f572318f32b1617f6054248e5888af68222f8e50",
6+ "sha256": "1icg3r70lg2kmd9gdc024ih1n9nrja98yav74z9nvykqygvv5w0n"
7+}
dep/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 @@
11# DO NOT HAND-EDIT THIS FILE
2−import ((import <nixpkgs> {}).fetchFromGitHub (
3− let json = builtins.fromJSON (builtins.readFile ./github.json);
4− in { inherit (json) owner repo rev sha256;
5− private = json.private or false;
6− }
7−))
2+let fetch = { private ? false, fetchSubmodules ? false, owner, repo, rev, sha256, ... }:
3+ if !fetchSubmodules && !private then builtins.fetchTarball {
4+ url = "https://github.com/${owner}/${repo}/archive/${rev}.tar.gz"; inherit sha256;
5+ } else (import <nixpkgs> {}).fetchFromGitHub {
6+ inherit owner repo rev sha256 fetchSubmodules private;
7+ };
8+in import (fetch (builtins.fromJSON (builtins.readFile ./github.json)))
dep/reflex-platform/github.json+5−4
@@ -1,7 +1,8 @@
11{
22 "owner": "reflex-frp",
33 "repo": "reflex-platform",
4− "branch": "develop",
5− "rev": "a7cd9a23e7faa9c2545a4c111229e28208e42351",
6− "sha256": "0y52rfrhk4zszgprpyni51l0pgq18dg695k5bmpd62c3zxar5mvm"
7−}
4+ "branch": "master",
5+ "private": false,
6+ "rev": "41be4d952b75515a037318aa344dd6b13ad29cfe",
7+ "sha256": "132yqzyzd3c5fjy7wwnwa5d6pjxv5ap2xz0swwbv3h5pwi8jwv0a"
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+
13# Fix misc upstream packages
24self: super:
35
46let
57 pkgs = self.callPackage ({ pkgs }: pkgs) {};
8+ haskellLib = pkgs.haskell.lib;
69in
710
811{
9− hnix = pkgs.haskell.lib.dontCheck (self.callCabal2nix "hnix" (pkgs.fetchFromGitHub {
10− owner = "haskell-nix";
11− repo = "hnix";
12− rev = "42afdc21da5d9e076eab57eaa42bfdde938192b8";
13− sha256 = "0psw384dx9bw2dp93xrzw8rd9amvcwgzn64jzzwby7sfspj6k349";
14− }) {});
15− # Need 8.0.2 build support
16− # PR: https://github.com/dmwit/universe/pull/33
17− universe-template = self.callCabal2nix "universe-template" (pkgs.fetchFromGitHub {
18− owner = "obsidiansystems";
19− repo = "universe";
20− rev = "6a71119bfa5db2b9990a2491c941469ff8ef5d13";
21− sha256 = "0z8smyainnlzcglv3dlx6x1n9j6d2jv48aa8f2421iayfkxg3js5";
22− } + /template) {};
12+ hnix = haskellLib.dontCheck (haskellLib.doJailbreak (self.callCabal2nix "hnix" (hackGet ../dep/hnix) {}));
13+ hnix-store-core = self.callHackage "hnix-store-core" "0.1.0.0" {};
14+
15+ ghcid = self.callCabal2nix "ghcid" (hackGet ../dep/ghcid) {};
16+ # Exports more internals
17+ snap-core = haskellLib.dontCheck (self.callCabal2nix "snap-core" (hackGet ../dep/snap-core) {});
18+
19+ shelly = self.callHackage "shelly" "1.9.0" {};
2320}
haskell-overlays/obelisk.nix+31−12
@@ -1,23 +1,42 @@
11# Add obelisk packages
2+
23self: super:
34
45let
56 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;
710in
811
912{
10− obelisk-executable-config = pkgs.obeliskExecutableConfig.haskellPackage self;
1113 obelisk-executable-config-inject = pkgs.obeliskExecutableConfig.platforms.web.inject self;
1214
13− obelisk-asset-manifest = self.callCabal2nix "obelisk-asset-manifest" ../lib/asset/manifest {};
14− obelisk-asset-serve-snap = self.callCabal2nix "obelisk-asset-serve-snap" ../lib/asset/serve-snap {};
15− obelisk-backend = self.callCabal2nix "obelisk-backend" (cleanHaskellSource ../lib/backend) {};
16− obelisk-cliapp = self.callCabal2nix "obelisk-cliapp" (cleanHaskellSource ../lib/cliapp) {};
17− obelisk-command = self.callCabal2nix "obelisk-command" (cleanHaskellSource ../lib/command) {};
18− obelisk-frontend = self.callCabal2nix "obelisk-frontend" (cleanHaskellSource ../lib/frontend) {};
19− obelisk-run = self.callCabal2nix "obelisk-run" (cleanHaskellSource ../lib/run) {};
20− obelisk-route = self.callCabal2nix "obelisk-route" (cleanHaskellSource ../lib/route) {};
21− obelisk-selftest = self.callCabal2nix "obelisk-selftest" (cleanHaskellSource ../lib/selftest) {};
22− obelisk-snap-extras = self.callCabal2nix "obelisk-snap-extras" (cleanHaskellSource ../lib/snap-extras) {};
15+ obelisk-asset-manifest = self.callCabal2nix "obelisk-asset-manifest" (obeliskCleanSource ../lib/asset/manifest) {};
16+ obelisk-asset-serve-snap = self.callCabal2nix "obelisk-asset-serve-snap" (obeliskCleanSource ../lib/asset/serve-snap) {};
17+ obelisk-backend = self.callCabal2nix "obelisk-backend" (obeliskCleanSource ../lib/backend) {};
18+ obelisk-cliapp = self.callCabal2nix "obelisk-cliapp" (obeliskCleanSource ../lib/cliapp) {};
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+ };
26+ obelisk-frontend = self.callCabal2nix "obelisk-frontend" (obeliskCleanSource ../lib/frontend) {};
27+ obelisk-run = onLinux (self.callCabal2nix "obelisk-run" (obeliskCleanSource ../lib/run) {}) (pkg:
28+ haskellLib.overrideCabal pkg (drv: { librarySystemDepends = [ pkgs.iproute ]; })
29+ );
30+ obelisk-route = self.callCabal2nix "obelisk-route" (obeliskCleanSource ../lib/route) {};
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+ };
40+ obelisk-snap-extras = self.callCabal2nix "obelisk-snap-extras" (obeliskCleanSource ../lib/snap-extras) {};
41+ tabulation = self.callCabal2nix "tabulation" (obeliskCleanSource ../lib/tabulation) {};
2342}
haskell-overlays/tighten-ob-exes.nix+11−30
@@ -3,26 +3,7 @@ self: super:
33let
44 pkgs = self.callPackage ({ pkgs }: pkgs) {};
55 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"
166
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− });
267 commandRuntimeDeps = with pkgs; [
278 coreutils
289 git
@@ -35,17 +16,17 @@ in
3516 # Dynamic linking with split objects dramatically increases startup time (about
3617 # 0.5 seconds on a decent machine with SSD), so we do `justStaticExecutables`.
3718 obelisk-command = haskellLib.overrideCabal
38− (addOptparseApplicativeCompletionScripts "ob"
39− (haskellLib.justStaticExecutables super.obelisk-command))
40− (drv: {
41− buildTools = (drv.buildTools or []) ++ [ pkgs.buildPackages.makeWrapper ];
42− postFixup = ''
43− ${drv.postFixup or ""}
44− # Make `ob` reference its runtime dependencies.
45− wrapProgram "$out"/bin/ob --prefix PATH : ${pkgs.lib.makeBinPath commandRuntimeDeps}
46− '';
47− passthru = { inherit commandRuntimeDeps; };
48− });
19+ (haskellLib.generateOptparseApplicativeCompletion "ob"
20+ (haskellLib.justStaticExecutables super.obelisk-command))
21+ (drv: {
22+ buildTools = (drv.buildTools or []) ++ [ pkgs.buildPackages.makeWrapper ];
23+ postFixup = ''
24+ ${drv.postFixup or ""}
25+ # Make `ob` reference its runtime dependencies.
26+ wrapProgram "$out"/bin/ob --prefix PATH : ${pkgs.lib.makeBinPath commandRuntimeDeps}
27+ '';
28+ passthru = { inherit commandRuntimeDeps; };
29+ });
4930
5031 obelisk-selftest = haskellLib.justStaticExecutables super.obelisk-selftest;
5132}
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 @@
11## obelisk-asset
22
3−The 'assets.nix' file contains nix expressions that are a mutually
4−recursive set of attributes used to create static asset directories
3+The 'assets.nix' file contains nix expressions that are a mutually
4+recursive set of attributes used to create static asset directories
55with hashable file encodings. This file should be incorporated within
66your project's 'default.nix' file. (See ./example/default.nix , line 2)
77```
@@ -10,20 +10,20 @@ your project's 'default.nix' file. (See ./example/default.nix , line 2)
1010```
1111
1212The 'mkAssets' and 'mkAssetsWith' functions within 'assets.nix' only
13−need to be passed an encoding('zopfliEncodings', 'gzipEncodings',
13+need to be passed an encoding('zopfliEncodings', 'gzipEncodings',
1414'noEncodings') and a static directory of your choice as arguments.
1515
16−In this example, 'mkAssets' is used on a directory that has a .png
16+In this example, 'mkAssets' is used on a directory that has a .png
1717file inside of it. (See ./example/default.nix, line 11)
1818```
19−myAssets = assets.mkAssets ./static;
19+myAssets = assets.mkAssets ./static;
2020```
2121
22−Once you have successfully incorporated 'assets.nix' into your
23−project's nix file(s), use nix-build to generate a symlink of
24−your 'mkAssets' expression. The following command from
25−example/run-example (line 3) will generate an immutable symlink
26−that holds hashed static assets.
22+Once you have successfully incorporated 'assets.nix' into your
23+project's nix file(s), use nix-build to generate a symlink of
24+your 'mkAssets' expression. The following command from
25+example/run-example (line 3) will generate an immutable symlink
26+that holds hashed static assets.
2727```bash
2828nix-build -o static.assets -A myAssets
2929```
lib/asset/manifest/obelisk-asset-manifest.cabal+1
@@ -17,6 +17,7 @@ library
1717 base
1818 , bytestring
1919 , containers
20+ , deepseq
2021 , directory
2122 , SHA
2223 , filepath
lib/asset/manifest/src/Obelisk/Asset/Gather.hs+5−2
@@ -1,10 +1,12 @@
11{-# LANGUAGE OverloadedStrings #-}
2+{-# LANGUAGE BangPatterns #-}
23-- | Functions for collecting up files to be processed by the asset pipeline
34module Obelisk.Asset.Gather
45 ( gatherHashedPaths
56 , toHashedPath
67 ) where
78
9+import Control.DeepSeq
810import Control.Monad (forM)
911import Data.Bits
1012import qualified Data.ByteString.Lazy as LBS
@@ -33,7 +35,7 @@ gatherHashedPaths root = go ""
3335 let relativePath = subdir </> sub
3436 isFile <- doesFileExist $ root </> relativePath
3537 if isFile
36− then do hashedRelativePath <- toHashedPath root relativePath
38+ then do !hashedRelativePath <- force <$> toHashedPath root relativePath
3739 return $ Map.singleton relativePath hashedRelativePath
3840 else go relativePath
3941
@@ -47,7 +49,8 @@ toHashedPath root relativePath = do
4749 contents <- LBS.readFile path
4850 let hashPrefix = T.unpack $ decodeUtf8 $ LBS.toStrict $ toNixBase32 $ bytestringDigest $ sha256 contents
4951 (dir, filename) = splitFileName relativePath
50− return $ normalise $ dir </> (hashPrefix <> "-" <> filename)
52+ !hashedRelativePath = force $ normalise $ dir </> (hashPrefix <> "-" <> filename)
53+ return hashedRelativePath
5154
5255-- | Convert a ByteString to base 32 in the way that Nix does
5356toNixBase32 :: LBS.ByteString -> LBS.ByteString
lib/asset/serve-snap/src/Obelisk/Asset/Accept.hs+6−6
@@ -136,16 +136,16 @@ qvalue = do
136136 skipMany lws
137137 q0 <|> q1
138138 where q0 = do
139− char '0'
139+ _ <- char '0'
140140 decimals <- option [] $ do
141− char '.'
141+ _ <- char '.'
142142 starRule (Just 0) (Just numAllowedDigits) digit
143143 return $ QValue $ MkFixed $ fromIntegral $ (read ('0' : decimals) :: Int) * 10 ^ (numAllowedDigits - length decimals)
144144 q1 = do
145− char '1'
145+ _ <- char '1'
146146 option () $ do
147− char '.'
148− starRule (Just 0) (Just numAllowedDigits) $ char '0'
147+ _ <- char '.'
148+ _ <- starRule (Just 0) (Just numAllowedDigits) $ char '0'
149149 return ()
150150 return $ QValue 1
151151 numAllowedDigits :: Int
@@ -232,7 +232,7 @@ hashRule minNum maxNum element = do
232232lws :: Parser ()
233233lws = do
234234 option () $ cr >> lf
235− starRule (Just 1) Nothing $ sp <|> ht
235+ _ <- starRule (Just 1) Nothing $ sp <|> ht
236236 return ()
237237
238238-- | See http://www.w3.org/Protocols/rfc2616/rfc2616-sec2.html#sec2.2
lib/asset/serve-snap/src/Obelisk/Asset/Serve/Snap.hs+21−10
@@ -12,13 +12,15 @@ import Obelisk.Asset.Accept (Encoding (..), acceptEncodingBody, chooseEncoding,
1212import Obelisk.Snap.Extras
1313
1414import 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)
1616import Snap.Util.FileServe (defaultMimeTypes, fileType, getSafePath, serveFile)
17+import Snap.Internal.Util.FileServe (checkRangeReq)
1718
1819import Control.Applicative ((<|>))
1920import Control.Exception (handleJust, try, throwIO)
20−import Control.Monad (forM, liftM)
21+import Control.Monad (forM, liftM, unless)
2122import Control.Monad.IO.Class (liftIO)
23+import Control.Monad.Fail (MonadFail)
2224import Data.Attoparsec.ByteString (parseOnly, endOfInput)
2325import Data.ByteString (ByteString)
2426import qualified Data.ByteString as BS
@@ -38,7 +40,7 @@ import System.Posix (getFileStatus, fileSize)
3840-- current version of the asset
3941-- (e.g. @0rx5yvkkgkig2pcqf4ngi1l7vh89qqajdzc6aiayaibyhvj0d853-test.png@). Hashed
4042-- 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 ()
4244serveAssets = serveAssets' True
4345
4446-- | 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
4749-- browser for the logical asset name (e.g. @test.png@) but instead the current
4850-- version of the asset with to point the browser at the unique filename for the
4951-- current version of the asset.
50−serveAssetsInPlace :: MonadSnap m => FilePath -> FilePath -> m ()
52+serveAssetsInPlace :: (MonadSnap m, MonadFail m) => FilePath -> FilePath -> m ()
5153serveAssetsInPlace = serveAssets' False
5254
5355-- | 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 ()
5557serveAsset = serveAsset' True
5658
5759-- | 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 ()
5961serveAssetInPlace = serveAsset' False
6062
6163-- | 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
6365-- 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
6466-- 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
6567-- 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 ()
6769serveAssets' doRedirect base fallback = do
6870 pRaw <- getSafePath
6971 serveAsset' doRedirect base fallback $ if "/" `isSuffixOf` pRaw || pRaw == "" then pRaw <> "index.html" else pRaw
7072
7173-- | 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 ()
7375serveAsset' doRedirect base fallback p = do
7476 assetType <- liftIO $ try $ BS.readFile $ base </> p </> "type"
7577 case assetType of
@@ -93,8 +95,17 @@ serveAsset' doRedirect base fallback p = do
9395 let finalFilename = base </> p </> "encodings" </> T.unpack (decodeUtf8 e)
9496 stat <- liftIO $ getFileStatus finalFilename
9597 modifyResponse $ setHeader "Last-Modified" "Thu, 1 Jan 1970 00:00:00 GMT"
96− modifyResponse $ setResponseCode 200 . setContentLength (fromIntegral $ fileSize stat) . setContentType (fileType defaultMimeTypes p)
97− sendFile finalFilename
98+ . setHeader "Accept-Ranges" "bytes"
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
108+ sendFile finalFilename
98109 Just _ -> cachePermanently >> modifyResponse (setResponseCode 304)
99110 Right "redirect" -> do
100111 mtarget <- liftIO $ getAssetTarget $ base </> p
lib/backend/obelisk-backend.cabal+3
@@ -8,6 +8,8 @@ library
88 build-depends: base,
99 bytestring,
1010 categories,
11+ containers,
12+ cookie,
1113 data-default,
1214 dependent-sum,
1315 dependent-sum-template,
@@ -15,6 +17,7 @@ library
1517 mtl,
1618 obelisk-asset-serve-snap,
1719 obelisk-executable-config-inject,
20+ obelisk-executable-config-lookup,
1821 obelisk-frontend,
1922 obelisk-route,
2023 obelisk-snap-extras,
lib/backend/src/Obelisk/Backend.hs+180−57
@@ -1,3 +1,5 @@
1+{-# LANGUAGE DeriveFunctor #-}
2+{-# LANGUAGE DeriveGeneric #-}
13{-# LANGUAGE FlexibleContexts #-}
24{-# LANGUAGE FlexibleInstances #-}
35{-# LANGUAGE GADTs #-}
@@ -6,62 +8,112 @@
68{-# LANGUAGE MultiParamTypeClasses #-}
79{-# LANGUAGE OverloadedStrings #-}
810{-# LANGUAGE ScopedTypeVariables #-}
9−{-# LANGUAGE TemplateHaskell #-}
11+{-# OPTIONS_GHC -fno-warn-orphans #-}
1012module Obelisk.Backend
1113 ( 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
1230 -- * Re-exports
1331 , Default (def)
1432 , getPageName
1533 , getRouteWith
1634 , runSnapWithCommandLineArgs
35+ , runSnapWithConfig
1736 , serveDefaultObeliskApp
1837 , prettifyOutput
19− , runBackend
2038 , staticRenderContentType
21− , mkRouteToUrl
39+ , getPublicConfigs
2240 ) where
2341
24−import Prelude hiding (id, (.))
25−import Control.Category
2642import Control.Monad
2743import Control.Monad.Except
28−import Control.Categorical.Bifunctor
44+import Control.Monad.Fail (MonadFail)
2945import Data.ByteString (ByteString)
3046import qualified Data.ByteString.Char8 as BSC8
3147import Data.Default (Default (..))
3248import Data.Dependent.Sum
33−import Data.Functor.Sum
3449import Data.Functor.Identity
50+import Data.Map (Map)
51+import qualified Data.Map as Map
3552import Data.Monoid ((<>))
3653import Data.Text (Text)
3754import qualified Data.Text as T
38−import Data.Text.Encoding
55+import GHC.Generics (Generic)
3956import Obelisk.Asset.Serve.Snap (serveAsset)
40−import Obelisk.ExecutableConfig.Inject (injectExecutableConfigs)
57+import qualified Obelisk.ExecutableConfig.Lookup as Lookup
4158import Obelisk.Frontend
4259import Obelisk.Route
4360import Obelisk.Snap.Extras (doNotCache, serveFileIfExistsAs)
4461import Reflex.Dom
4562import Snap (MonadSnap, Snap, commandLineConfig, defaultConfig, getsRequest, httpServe, modifyResponse
46− , rqPathInfo, rqQueryString, setContentType, writeBS, writeText)
63+ , rqPathInfo, rqQueryString, setContentType, writeBS, writeText
64+ , rqCookies, Cookie(..) , setHeader)
4765import Snap.Internal.Http.Server.Config (Config (accessLog, errorLog), ConfigLog (ConfigIoLog))
4866import System.IO (BufferMode (..), hSetBuffering, stderr, stdout)
4967
5068data 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
5270 , _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)
5480
5581-- | The static assets provided must contain a compiled GHCJS app that corresponds exactly to the Frontend provided
5682data GhcjsApp route = GhcjsApp
5783 { _ghcjsApp_compiled :: !StaticAssets
5884 , _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
59102 }
60103
61104-- | Serve a frontend, which must be the same frontend that Obelisk has built and placed in the default location
62105--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 ()
64−serveDefaultObeliskApp urlEnc serveStaticAsset frontend = serveObeliskApp urlEnc serveStaticAsset frontendApp
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 ()
115+serveDefaultObeliskApp urlEnc ghcjsWidgets serveStaticAsset frontend =
116+ serveObeliskApp urlEnc ghcjsWidgets serveStaticAsset frontendApp
65117 where frontendApp = GhcjsApp
66118 { _ghcjsApp_compiled = defaultFrontendGhcjsAssets
67119 , _ghcjsApp_value = frontend
@@ -86,45 +138,56 @@ defaultFrontendGhcjsAssets = StaticAssets
86138 , _staticAssets_unprocessed = "frontend.jsexe"
87139 }
88140
89−runSnapWithCommandLineArgs :: Snap () -> IO ()
90−runSnapWithCommandLineArgs a = do
91− -- Get the web server configuration from the command line
92− cmdLineConf <- commandLineConfig defaultConfig
93− let httpConf = cmdLineConf
141+runSnapWithConfig :: MonadIO m => Config Snap a -> Snap () -> m ()
142+runSnapWithConfig conf a = do
143+ let httpConf = conf
94144 { accessLog = Just $ ConfigIoLog BSC8.putStrLn
95145 , errorLog = Just $ ConfigIoLog BSC8.putStrLn
96146 }
97147 -- 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
99154
100155getPageName :: (MonadSnap m) => m PageName
101156getPageName = do
102157 p <- getsRequest rqPathInfo
103158 q <- getsRequest rqQueryString
104− let pageNameEncoder' :: Encoder Identity Identity PageName (String, String)
105− pageNameEncoder' = bimap
106− (unpackTextEncoder . pathSegmentsTextEncoder . listToNonEmptyEncoder)
107− (unpackTextEncoder . queryParametersTextEncoder . toListMapEncoder)
108− return $ decode pageNameEncoder' (T.unpack (decodeUtf8 p), T.unpack (decodeUtf8 q))
159+ return $ byteStringsToPageName p q
109160
110161getRouteWith :: (MonadSnap m) => Encoder Identity parse route PageName -> m (parse route)
111162getRouteWith e = do
112163 pageName <- getPageName
113164 return $ tryDecode e pageName
114165
115−serveObeliskApp :: MonadSnap m => (R appRoute -> Text) -> ([Text] -> m ()) -> GhcjsApp (R appRoute) -> R (ObeliskRoute appRoute) -> m ()
116−serveObeliskApp urlEnc serveStaticAsset frontendApp = \case
117− ObeliskRoute_App appRouteComponent :=> Identity appRouteRest -> serveGhcjsApp urlEnc frontendApp $ GhcjsAppRoute_App appRouteComponent :/ appRouteRest
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
177+ -> R (ObeliskRoute appRoute)
178+ -> m ()
179+serveObeliskApp urlEnc ghcjsWidgets serveStaticAsset frontendApp config = \case
180+ ObeliskRoute_App appRouteComponent :=> Identity appRouteRest -> serveGhcjsApp urlEnc ghcjsWidgets frontendApp config $ GhcjsAppRoute_App appRouteComponent :/ appRouteRest
118181 ObeliskRoute_Resource resComponent :=> Identity resRest -> case resComponent :=> Identity resRest of
119182 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
121184 ResourceRoute_JSaddleWarp :=> Identity _ -> do
122185 let msg = "Error: Obelisk.Backend received jsaddle request"
123186 liftIO $ putStrLn $ T.unpack msg
124187 writeText msg
125188 ResourceRoute_Version :=> Identity () -> doNotCache >> serveFileIfExistsAs "text/plain" "version"
126189
127−serveStaticAssets :: MonadSnap m => StaticAssets -> [Text] -> m ()
190+serveStaticAssets :: (MonadSnap m, MonadFail m) => StaticAssets -> [Text] -> m ()
128191serveStaticAssets assets pathSegments = serveAsset (_staticAssets_processed assets) (_staticAssets_unprocessed assets) $ T.unpack $ T.intercalate "/" pathSegments
129192
130193data StaticAssets = StaticAssets
@@ -141,35 +204,95 @@ staticRenderContentType :: ByteString
141204staticRenderContentType = "text/html; charset=utf-8"
142205
143206--TODO: Don't assume we're being served at "/"
144−serveGhcjsApp :: MonadSnap m => (R appRouteComponent -> Text) -> GhcjsApp (R appRouteComponent) -> R (GhcjsAppRoute appRouteComponent) -> m ()
145−serveGhcjsApp urlEnc app = \case
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
213+ -> R (GhcjsAppRoute appRouteComponent)
214+ -> m ()
215+serveGhcjsApp urlEnc ghcjsWidgets app config = \case
146216 GhcjsAppRoute_App appRouteComponent :=> Identity appRouteRest -> do
147217 modifyResponse $ setContentType staticRenderContentType
148− writeBS <=< liftIO $ renderGhcjsFrontend urlEnc (appRouteComponent :/ appRouteRest) $ _ghcjsApp_value app
218+ modifyResponse $ setHeader "Cache-Control" "no-store private"
219+ writeBS <=< renderGhcjsFrontend urlEnc ghcjsWidgets (appRouteComponent :/ appRouteRest) config $ _ghcjsApp_value app
149220 GhcjsAppRoute_Resource :=> Identity pathSegments -> serveStaticAssets (_ghcjsApp_compiled app) pathSegments
150221
151−runBackend :: Backend fullRoute frontendRoute -> Frontend (R frontendRoute) -> IO ()
152−runBackend backend frontend = case checkEncoder $ _backend_routeEncoder backend of
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.
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 ()
236+runBackendWith (BackendConfig runSnap staticAssets ghcjsWidgets) backend frontend = case checkEncoder $ _backend_routeEncoder backend of
153237 Left e -> fail $ "backend error:\n" <> T.unpack e
154− Right validFullEncoder -> _backend_run backend $ \serveRoute -> do
155− runSnapWithCommandLineArgs $ do
156− getRouteWith validFullEncoder >>= \case
157− Identity r -> case r of
158− InL backendRoute :=> Identity a -> serveRoute $ backendRoute :/ a
159− InR obeliskRoute :=> Identity a ->
160− serveDefaultObeliskApp (mkRouteToUrl validFullEncoder) (serveStaticAssets defaultStaticAssets) frontend $ obeliskRoute :/ a
161−
162−mkRouteToUrl :: Encoder Identity parse (R (Sum f (ObeliskRoute r))) PageName -> R r -> Text
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
166−
167−
168−renderGhcjsFrontend :: MonadIO m => (route -> Text) -> route -> Frontend route -> m ByteString
169−renderGhcjsFrontend urlEnc route f = do
170− let baseTag = elAttr "base" ("href" =: "/") blank --TODO: Figure out the base URL from the routes
171− ghcjsPreload = elAttr "link" ("rel" =: "preload" <> "as" =: "script" <> "href" =: "ghcjs/all.js") blank
172− ghcjsScript = elAttr "script" ("language" =: "javascript" <> "src" =: "ghcjs/all.js" <> "defer" =: "defer") blank
173− liftIO $ renderFrontendHtml urlEnc route
174− (_frontend_head f >> injectExecutableConfigs >> baseTag >> ghcjsPreload)
175− (_frontend_body f >> ghcjsScript)
238+ Right validFullEncoder -> do
239+ publicConfigs <- getPublicConfigs
240+ _backend_run backend $ \serveRoute ->
241+ runSnap $
242+ getRouteWith validFullEncoder >>= \case
243+ Identity r -> case r of
244+ FullRoute_Backend backendRoute :/ a -> serveRoute $ backendRoute :/ a
245+ FullRoute_Frontend obeliskRoute :/ a ->
246+ serveDefaultObeliskApp routeToUrl (($ allJsUrl) <$> ghcjsWidgets) (serveStaticAssets staticAssets) frontend publicConfigs $
247+ obeliskRoute :/ a
248+ where
249+ routeToUrl (k :/ v) = renderObeliskRoute validFullEncoder $ FullRoute_Frontend (ObeliskRoute_App k) :/ v
250+ allJsUrl = renderAllJsPath validFullEncoder
251+
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
260+renderGhcjsFrontend urlEnc ghcjsWidgets route configs f = do
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 ()
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 ()
272+deferredGhcjsScript allJsUrl = elAttr "script" ("type" =: "text/javascript" <> "src" =: allJsUrl <> "defer" =: "defer") blank
273+
274+-- | An all.js script which is loaded after waiting for some time to pass. This
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
77library
88 hs-source-dirs: src
99 build-depends:
10− ansi-terminal
10+ aeson
11+ , ansi-terminal
1112 , base
1213 , bytestring
14+ , containers
1315 , exceptions
1416 , io-streams
1517 , lens
@@ -23,11 +25,11 @@ library
2325 , transformers
2426 exposed-modules:
2527 Obelisk.CliApp
26− Obelisk.CliApp.Demo
2728 other-modules:
2829 Obelisk.CliApp.Logging
2930 Obelisk.CliApp.Process
3031 Obelisk.CliApp.Spinner
3132 Obelisk.CliApp.TerminalString
33+ Obelisk.CliApp.Theme
3234 Obelisk.CliApp.Types
3335 ghc-options: -Wall -fobject-code
lib/cliapp/src/Obelisk/CliApp.hs+15−6
@@ -33,16 +33,25 @@ module Obelisk.CliApp
3333 , Severity (..)
3434
3535 -- .Process
36− , ProcessFailure (..)
3736 , AsProcessFailure (..)
38− , readProcessAndLogStderr
39− , readProcessAndLogOutput
40− , readCreateProcessWithExitCode
37+ , ProcessFailure (..)
38+ , ProcessSpec (..)
39+ , callCommand
40+ , callProcess
4141 , callProcessAndLogOutput
4242 , createProcess_
43− , callProcess
44− , callCommand
43+ , overCreateProcess
44+ , proc
45+ , readCreateProcessWithExitCode
46+ , readProcessAndLogOutput
47+ , readProcessAndLogStderr
48+ , readProcessJSONAndLogStderr
4549 , reconstructCommand
50+ , setCwd
51+ , setDelegateCtlc
52+ , setEnvOverride
53+ , shell
54+ , waitForProcess
4655 ) where
4756
4857import 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 ((<>))
4343import Data.Text (Text)
4444import qualified Data.Text as T
4545import qualified Data.Text.IO as T
46+import GHC.IO.Encoding.Types
4647import System.Console.ANSI (Color (Red, Yellow), ColorIntensity (Vivid),
4748 ConsoleIntensity (FaintIntensity), ConsoleLayer (Foreground),
4849 SGR (SetColor, SetConsoleIntensity), clearLine)
4950import System.Exit (ExitCode (..))
50−import System.IO (BufferMode (NoBuffering), hFlush, hReady, hSetBuffering, stderr, stdin, stdout)
51+import System.IO
5152
5253import qualified Obelisk.CliApp.TerminalString as TS
54+import Obelisk.CliApp.Theme
5355import Obelisk.CliApp.Types
5456
5557newCliConfig
@@ -63,7 +65,11 @@ newCliConfig sev noColor noSpinner errorLogExitCode = do
6365 lock <- newMVar False
6466 tipDisplayed <- newIORef False
6567 stack <- newIORef ([], [])
66− return $ CliConfig level noColor noSpinner lock tipDisplayed stack errorLogExitCode
68+ textEncoding <- hGetEncoding stdout
69+ let theme = if maybe False supportsUnicode textEncoding
70+ then unicodeTheme
71+ else noUnicodeTheme
72+ return $ CliConfig level noColor noSpinner lock tipDisplayed stack errorLogExitCode theme
6773
6874runCli :: MonadIO m => CliConfig e -> CliT e m a -> m a
6975runCli c =
@@ -126,7 +132,7 @@ handleLog' noColor output = do
126132 hFlush stdout
127133 Output_Overwrite ts -> liftIO $ do
128134 width <- TS.getTerminalWidth
129− T.putStr $ "\r" <> (TS.render (not noColor) width ts)
135+ T.putStr $ "\r" <> TS.render (not noColor) width ts
130136 hFlush stdout
131137 Output_ClearLine -> liftIO $ do
132138 -- Go to the first column and clear the whole line
@@ -172,14 +178,16 @@ withExitFailMessage msg f = f `catch` \(e :: ExitCode) -> do
172178
173179-- | Write log to stdout, with colors (unless `noColor`)
174180writeLog :: (MonadIO m, MonadMask m) => Bool -> Bool -> WithSeverity Text -> m ()
175−writeLog withNewLine noColor (WithSeverity severity s)
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+writeLog withNewLine noColor (WithSeverity severity s) = if T.null s then pure () else write
181182 where
182− putFn = if withNewLine then (T.hPutStrLn h) else (T.hPutStr h)
183+ write
184+ | noColor && severity <= Warning = liftIO $ putFn $ T.pack (show severity) <> ": " <> s
185+ | not noColor && severity <= Error = TS.putStrWithSGR errorColors h withNewLine s
186+ | not noColor && severity <= Warning = TS.putStrWithSGR warningColors h withNewLine s
187+ | not noColor && severity >= Debug = TS.putStrWithSGR debugColors h withNewLine s
188+ | otherwise = liftIO $ putFn s
189+
190+ putFn = if withNewLine then T.hPutStrLn h else T.hPutStr h
183191 h = if severity <= Error then stderr else stdout
184192 errorColors = [SetColor Foreground Vivid Red]
185193 warningColors = [SetColor Foreground Vivid Yellow]
@@ -226,3 +234,20 @@ fork :: (HasCliConfig e m, MonadIO m) => CliT e IO () -> m ThreadId
226234fork f = do
227235 c <- getCliConfig
228236 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 @@
99-- | An extension of `System.Process` that integrates with logging (`Obelisk.CLI.Logging`)
1010-- and is thus spinner friendly.
1111module Obelisk.CliApp.Process
12− ( ProcessFailure (..)
13− , AsProcessFailure (..)
14− , readProcessAndLogStderr
15− , readProcessAndLogOutput
16− , readCreateProcessWithExitCode
12+ ( AsProcessFailure (..)
13+ , ProcessFailure (..)
14+ , ProcessSpec (..)
15+ , callCommand
16+ , callProcess
1717 , callProcessAndLogOutput
1818 , createProcess
1919 , createProcess_
20− , callProcess
21− , callCommand
20+ , overCreateProcess
21+ , proc
22+ , readCreateProcessWithExitCode
23+ , readProcessAndLogOutput
24+ , readProcessAndLogStderr
25+ , readProcessJSONAndLogStderr
2226 , reconstructCommand
27+ , setCwd
28+ , setDelegateCtlc
29+ , setEnvOverride
30+ , shell
31+ , waitForProcess
2332 ) where
2433
2534import Control.Monad ((<=<), join, void)
2635import Control.Monad.Except (throwError)
36+import Control.Monad.Fail
2737import Control.Monad.IO.Class (MonadIO, liftIO)
2838import Control.Lens (Prism', review)
2939import qualified Data.ByteString as BS
3040import qualified Data.ByteString.Char8 as BSC
3141import Data.Function (fix)
42+import Data.Map (Map)
43+import qualified Data.Map as Map
3244import Data.Monoid ((<>))
3345import Data.Text (Text)
3446import qualified Data.Text as T
3547import qualified Data.Text.Encoding as T
48+import Data.Text.Encoding.Error (lenientDecode)
49+import System.Environment (getEnvironment)
3650import System.Exit (ExitCode (..))
3751import System.IO (Handle)
3852import System.IO.Streams (InputStream, handleToInputStream)
3953import qualified System.IO.Streams as Streams
4054import System.IO.Streams.Concurrent (concurrentMerge)
41−import System.Process (CreateProcess, ProcessHandle, StdStream (CreatePipe), cmdspec, std_err, std_out,
42− waitForProcess)
55+import System.Process (CreateProcess, ProcessHandle, StdStream (CreatePipe), std_err, std_out)
4356import qualified System.Process as Process
57+import qualified Data.Aeson as Aeson
4458
4559import Control.Monad.Log (Severity (..))
4660import Obelisk.CliApp.Logging (putLog, putLogRaw)
4761import Obelisk.CliApp.Types (CliLog, CliThrow)
4862
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+
4987-- TODO put back in `Obelisk.CliApp.Process` and use prisms for extensible exceptions
5088data ProcessFailure = ProcessFailure Process.CmdSpec Int -- exit code
5189 deriving Show
@@ -59,18 +97,32 @@ instance AsProcessFailure ProcessFailure where
5997 asProcessFailure = id
6098
6199readProcessAndLogStderr
62− :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e)
63− => Severity -> CreateProcess -> m Text
100+ :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e, MonadFail m)
101+ => Severity -> ProcessSpec -> m Text
64102readProcessAndLogStderr sev process = do
65103 (out, _err) <- withProcess process $ \_out err -> do
66104 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
68119
69120readCreateProcessWithExitCode
70121 :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e)
71− => CreateProcess -> m (ExitCode, String, String)
72−readCreateProcessWithExitCode process = do
73− putLog Debug $ "Creating process: " <> reconstructCommand (cmdspec process)
122+ => ProcessSpec -> m (ExitCode, String, String)
123+readCreateProcessWithExitCode procSpec = do
124+ process <- mkCreateProcess procSpec
125+ putLog Debug $ "Creating process: " <> reconstructProcSpec procSpec
74126 liftIO $ Process.readCreateProcessWithExitCode process ""
75127
76128-- | Like `System.Process.readProcess` but logs the combined output (stdout and stderr)
@@ -81,22 +133,22 @@ readCreateProcessWithExitCode process = do
81133-- which case it is advisable to call it with a non-Error severity for stderr, like
82134-- `callProcessAndLogOutput (Debug, Debug)`.
83135readProcessAndLogOutput
84− :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e)
85− => (Severity, Severity) -> CreateProcess -> m Text
136+ :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e, MonadFail m)
137+ => (Severity, Severity) -> ProcessSpec -> m Text
86138readProcessAndLogOutput (sev_out, sev_err) process = do
87− (_, Just out, Just err, p) <- createProcess $ process
88− { std_out = CreatePipe , std_err = CreatePipe }
139+ (_, Just out, Just err, p) <- createProcess $ overCreateProcess
140+ (\p -> p { std_out = CreatePipe , std_err = CreatePipe }) process
89141
90142 -- TODO interleave stdout and stderr in log correctly
91143 streamToLog =<< liftIO (streamHandle sev_err err)
92− outText <- liftIO $ T.decodeUtf8 <$> BS.hGetContents out
144+ outText <- liftIO $ T.decodeUtf8With lenientDecode <$> BS.hGetContents out
93145 putLogRaw sev_out outText
94146
95− liftIO (waitForProcess p) >>= \case
147+ waitForProcess p >>= \case
96148 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
98150
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)
100152-- with the corresponding severity.
101153--
102154-- Usually this function is called as `callProcessAndLogOutput (Debug, Error)`. However
@@ -104,9 +156,8 @@ readProcessAndLogOutput (sev_out, sev_err) process = do
104156-- which case it is advisable to call it with a non-Error severity for stderr, like
105157-- `callProcessAndLogOutput (Debug, Debug)`.
106158callProcessAndLogOutput
107−
108− :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e)
109− => (Severity, Severity) -> CreateProcess -> m ()
159+ :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e, MonadFail m)
160+ => (Severity, Severity) -> ProcessSpec -> m ()
110161callProcessAndLogOutput (sev_out, sev_err) process =
111162 void $ withProcess process $ \out err -> do
112163 stream <- liftIO $ join $ combineStream
@@ -116,21 +167,31 @@ callProcessAndLogOutput (sev_out, sev_err) process =
116167 where
117168 combineStream s1 s2 = concurrentMerge [s1, s2]
118169
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
120171createProcess
121172 :: (MonadIO m, CliLog m)
122− => CreateProcess -> m (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)
123−createProcess p = do
124− putLog Debug $ "Creating process: " <> reconstructCommand (cmdspec p)
173+ => ProcessSpec -> m (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)
174+createProcess procSpec = do
175+ p <- mkCreateProcess procSpec
176+ putLog Debug $ "Creating process: " <> reconstructProcSpec procSpec
125177 liftIO $ Process.createProcess p
126178
127179-- | Like `System.Process.createProcess_` but also logs (debug) the process being run
128180createProcess_
129181 :: (MonadIO m, CliLog m)
130− => String -> CreateProcess -> m (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)
131−createProcess_ name p = do
132− putLog Debug $ "Creating process " <> T.pack name <> ": " <> reconstructCommand (cmdspec p)
133− liftIO $ Process.createProcess p
182+ => String -> ProcessSpec -> m (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)
183+createProcess_ name procSpec = do
184+ p <- mkCreateProcess procSpec
185+ putLog Debug $ "Creating process " <> T.pack name <> ": " <> reconstructProcSpec procSpec
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) }
134195
135196-- | Like `System.Process.callProcess` but also logs (debug) the process being run
136197callProcess
@@ -149,18 +210,18 @@ callCommand cmd = do
149210 liftIO $ Process.callCommand cmd
150211
151212withProcess
152− :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e)
153− => CreateProcess -> (Handle -> Handle -> m ()) -> m (Handle, Handle)
213+ :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e, MonadFail m)
214+ => ProcessSpec -> (Handle -> Handle -> m ()) -> m (Handle, Handle)
154215withProcess process f = do -- TODO: Use bracket.
155216 -- FIXME: Using `withCreateProcess` here leads to something operating illegally on closed handles.
156− (_, Just out, Just err, p) <- createProcess $ process
157− { std_out = CreatePipe , std_err = CreatePipe }
217+ (_, Just out, Just err, p) <- createProcess $ overCreateProcess
218+ (\x -> x { std_out = CreatePipe , std_err = CreatePipe }) process
158219
159220 f out err -- Pass the handles to the passed function
160221
161− liftIO (waitForProcess p) >>= \case
222+ waitForProcess p >>= \case
162223 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
164225
165226-- Create an input stream from the file handle, associating each item with the given severity.
166227streamHandle :: Severity -> Handle -> IO (InputStream (Severity, BSC.ByteString))
@@ -173,12 +234,20 @@ streamToLog
173234streamToLog stream = fix $ \loop -> do
174235 liftIO (Streams.read stream) >>= \case
175236 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
177242
178243-- | Pretty print a 'CmdSpec'
179244reconstructCommand :: Process.CmdSpec -> Text
180−reconstructCommand (Process.ShellCommand str) = T.pack str
181−reconstructCommand (Process.RawCommand c as) = processToShellString c as
245+reconstructCommand p = case p of
246+ Process.ShellCommand str -> T.pack str
247+ Process.RawCommand c as -> processToShellString c as
182248 where
183249 processToShellString cmd args = T.unwords $ map quoteAndEscape (cmd : args)
184250 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))
2424
2525import Obelisk.CliApp.Logging (allowUserToMakeLoggingVerbose, fork, putLog)
2626import Obelisk.CliApp.TerminalString (TerminalString (..), enquiryCode)
27+import Obelisk.CliApp.Theme
2728import Obelisk.CliApp.Types (CliLog, CliConfig (..), HasCliConfig, Output (..), getCliConfig)
2829
2930-- | Run an action with a CLI spinner.
@@ -51,7 +52,8 @@ withSpinner'
5152 -> m a
5253 -> m a
5354withSpinner' msg mkTrail action = do
54− noSpinner <- _cliConfig_noSpinner <$> getCliConfig
55+ cliConf <- getCliConfig
56+ let noSpinner = _cliConfig_noSpinner cliConf
5557 if noSpinner
5658 then putLog Notice msg >> action
5759 else bracket' run cleanup $ const action
@@ -61,8 +63,11 @@ withSpinner' msg mkTrail action = do
6163 modifyStack pushSpinner >>= \case
6264 True -> do -- Top-level spinner; fork a thread to manage output of anything on the stack
6365 ctrleThread <- fork $ allowUserToMakeLoggingVerbose enquiryCode
66+ cliConf <- getCliConfig
67+ let theme = _cliConfig_theme cliConf
68+ spinner = coloredSpinner $ _cliTheme_spinner theme
6469 spinnerThread <- fork $ runSpinner spinner $ \c -> do
65− logs <- renderSpinnerStack c . snd <$> readStack
70+ logs <- renderSpinnerStack theme c . snd <$> readStack
6671 logMessage $ Output_Overwrite logs
6772 pure [ctrleThread, spinnerThread]
6873 False -> -- Sub-spinner; nothing to do.
@@ -70,13 +75,15 @@ withSpinner' msg mkTrail action = do
7075 cleanup tids resultM = do
7176 liftIO $ mapM_ killThread tids
7277 logMessage Output_ClearLine
73− logsM <- modifyStack $ popSpinner $ case resultM of
78+ cliConf <- getCliConfig
79+ let theme = _cliConfig_theme cliConf
80+ logsM <- modifyStack $ (popSpinner theme) $ case resultM of
7481 Nothing ->
75− ( TerminalString_Colorized Red "✖"
82+ ( TerminalString_Colorized Red $ _cliTheme_failed $ _cliConfig_theme cliConf
7683 , Just msg -- Always display final message if there was an exception.
7784 )
7885 Just result ->
79− ( TerminalString_Colorized Green "✔"
86+ ( TerminalString_Colorized Green $ _cliTheme_done $ _cliConfig_theme cliConf
8087 , mkTrail <*> pure result
8188 )
8289 -- Last message, finish off with newline.
@@ -87,10 +94,10 @@ withSpinner' msg mkTrail action = do
8794 )
8895 where
8996 isTemporary = isNothing mkTrail
90− popSpinner (mark, trailMsgM) (flag, old) =
97+ popSpinner theme (mark, trailMsgM) (flag, old) =
9198 ( (newFlag, new)
9299 -- With final trail spinner message to render
93− , renderSpinnerStack mark . (: new) . TerminalString_Normal <$> (
100+ , renderSpinnerStack theme mark . (: new) . TerminalString_Normal <$> (
94101 if inTemporarySpinner then Nothing else trailMsgM
95102 )
96103 )
@@ -102,19 +109,19 @@ withSpinner' msg mkTrail action = do
102109 =<< fmap _cliConfig_spinnerStack getCliConfig
103110 modifyStack f = liftIO . flip atomicModifyIORef' f
104111 =<< fmap _cliConfig_spinnerStack getCliConfig
105− spinner = coloredSpinner defaultSpinnerTheme
106112
107113-- | How nested spinner logs should be displayed
108114renderSpinnerStack
109− :: TerminalString -- ^ That which comes before the final element in stack
115+ :: CliTheme
116+ -> TerminalString -- ^ That which comes before the final element in stack
110117 -> [TerminalString] -- ^ Spinner elements in reverse order
111118 -> [TerminalString]
112−renderSpinnerStack mark = L.intersperse space . go . L.reverse
119+renderSpinnerStack theme mark = L.intersperse space . go . L.reverse
113120 where
114121 go [] = []
115122 go (x:[]) = mark : [x]
116123 go (x:xs) = arrow : x : go xs
117− arrow = TerminalString_Colorized Blue "⇾"
124+ arrow = TerminalString_Colorized Blue $ _cliTheme_arrow theme
118125 space = TerminalString_Normal " "
119126
120127-- | 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
130137 where
131138 delay = liftIO $ threadDelay 100000 -- A shorter delay ensures that we update promptly.
132139
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−
145140-- | Like `bracket` but the `release` function can know whether an exception was raised
146141bracket' :: MonadMask m => m a -> (a -> Maybe c -> m b) -> (a -> m c) -> m c
147142bracket' 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 @@
66{-# LANGUAGE GeneralizedNewtypeDeriving #-}
77{-# LANGUAGE MultiParamTypeClasses #-}
88{-# LANGUAGE OverloadedStrings #-}
9+{-# LANGUAGE StandaloneDeriving #-}
910{-# LANGUAGE UndecidableInstances #-}
1011module Obelisk.CliApp.Types where
1112
1213import Control.Concurrent.MVar (MVar)
1314import Control.Monad.Catch (MonadCatch, MonadMask, MonadThrow)
14−import Control.Monad.Log (LoggingT, MonadLog, Severity (..), WithSeverity (..), logMessage)
15+import Control.Monad.Fail (MonadFail)
16+import Control.Monad.Log (LoggingT(..), MonadLog, Severity (..), WithSeverity (..), logMessage)
1517import Control.Monad.Reader (MonadIO, ReaderT (..), MonadReader (..), ask, mapReaderT)
1618import Control.Monad.Writer (WriterT)
1719import Control.Monad.State (StateT)
@@ -23,6 +25,7 @@ import Data.Text (Text)
2325import System.Exit (ExitCode (..), exitWith)
2426
2527import Obelisk.CliApp.TerminalString (TerminalString)
28+import Obelisk.CliApp.Theme (CliTheme)
2629
2730--------------------------------------------------------------------------------
2831
@@ -44,9 +47,11 @@ type CliThrow e m = MonadError e m
4447putLog :: CliLog m => Severity -> Text -> m ()
4548putLog sev = logMessage . Output_Log . WithSeverity sev
4649
50+deriving instance MonadFail m => MonadFail (LoggingT Output m)
51+
4752newtype DieT e m a = DieT { unDieT :: ReaderT (e -> (Text, Int)) (LoggingT Output m) a }
4853 deriving
49− ( Functor, Applicative, Monad, MonadIO
54+ ( Functor, Applicative, Monad, MonadIO, MonadFail
5055 , MonadThrow, MonadCatch, MonadMask
5156 , MonadLog Output
5257 )
@@ -88,6 +93,8 @@ data CliConfig e = CliConfig
8893 _cliConfig_spinnerStack :: IORef ([Bool], [TerminalString])
8994 , -- | Failure handler. How to log error and what exit status to use.
9095 _cliConfig_errorLogExitCode :: e -> (Text, Int)
96+ , -- | Theme strings for spinners
97+ _cliConfig_theme :: CliTheme
9198 }
9299
93100class Monad m => HasCliConfig e m | m -> e where
@@ -111,7 +118,7 @@ newtype CliT e m a = CliT
111118 { unCliT :: ReaderT (CliConfig e) (DieT e m) a
112119 }
113120 deriving
114− ( Functor, Applicative, Monad, MonadIO
121+ ( Functor, Applicative, Monad, MonadIO, MonadFail
115122 , MonadThrow, MonadCatch, MonadMask
116123 , MonadLog Output -- CliLog
117124 , MonadError e -- CliThrow
lib/command/obelisk-command.cabal+11−12
@@ -1,5 +1,5 @@
11name: obelisk-command
2−version: 0.1
2+version: 0.7.0.1
33cabal-version: >= 1.8
44build-type: Simple
55
@@ -7,35 +7,33 @@ library
77 hs-source-dirs: src
88 build-depends:
99 aeson
10− , ansi-terminal
11− , Cabal
1210 , aeson-pretty
11+ , ansi-terminal
1312 , base
14− , base16-bytestring
15− , binary
1613 , bytestring
17− , obelisk-cliapp
14+ , Cabal
1815 , containers
1916 , data-default
2017 , directory
2118 , either
2219 , errors
20+ , exceptions
2321 , filepath
22+ , git
2423 , github
2524 , here
26− , hit
2725 , hnix
2826 , hpack
2927 , io-streams
30− , exceptions
31− , logging-effect
3228 , lens
29+ , logging-effect
30+ , megaparsec
3331 , modern-uri
3432 , monad-loops
3533 , mtl
36− , megaparsec
3734 , network
3835 , network-uri
36+ , obelisk-cliapp
3937 , optparse-applicative
4038 , placeholders
4139 , process
@@ -46,6 +44,7 @@ library
4644 , transformers
4745 , unix
4846 , unordered-containers
47+ , which
4948 , yaml
5049 exposed-modules:
5150 Obelisk.App
@@ -57,8 +56,8 @@ library
5756 Obelisk.Command.Thunk
5857 Obelisk.Command.Utils
5958 Obelisk.Command.VmBuilder
60− -- -fobject-code is so that the StaticPointers extension can work in ghci
61− ghc-options: -Wall -fobject-code
59+ Obelisk.Command.Preprocessor
60+ ghc-options: -Wall
6261
6362executable ob
6463 main-is: src-bin/ob.hs
lib/command/src/Obelisk/App.hs+5−2
@@ -12,6 +12,7 @@ module Obelisk.App where
1212
1313import Control.Lens
1414import Control.Monad.Catch (MonadCatch, MonadMask, MonadThrow)
15+import Control.Monad.Fail (MonadFail)
1516import Control.Monad.Reader (MonadIO, ReaderT (..), ask, runReaderT)
1617import Control.Monad.Writer (WriterT)
1718import Control.Monad.State (StateT)
@@ -61,10 +62,11 @@ newtype ObeliskT m a = ObeliskT
6162 { unObeliskT :: ReaderT Obelisk (CliT ObeliskError m) a
6263 }
6364 deriving
64− ( Functor, Applicative, Monad, MonadIO, MonadThrow, MonadCatch, MonadMask
65+ ( Functor, Applicative, Monad, MonadIO, MonadThrow, MonadCatch, MonadMask, MonadFail
6566 , MonadLog Output -- CliLog
6667 , MonadError ObeliskError -- CliThrow ObeliskError
67− , HasCliConfig ObeliskError)
68+ , HasCliConfig ObeliskError
69+ )
6870
6971instance MonadTrans ObeliskT where
7072 lift = ObeliskT . lift . lift
@@ -104,6 +106,7 @@ type MonadInfallibleObelisk m =
104106type MonadObelisk m =
105107 ( MonadInfallibleObelisk m
106108 , CliThrow ObeliskError m
109+ , MonadFail m
107110 )
108111
109112getObeliskUserStateDir :: IO FilePath
lib/command/src/Obelisk/Command.hs+122−86
@@ -3,22 +3,16 @@
33{-# LANGUAGE MultiParamTypeClasses #-}
44{-# LANGUAGE OverloadedStrings #-}
55{-# LANGUAGE ScopedTypeVariables #-}
6−{-# LANGUAGE StaticPointers #-}
7−{-# LANGUAGE TypeApplications #-}
86module Obelisk.Command where
97
108import Control.Monad
119import Control.Monad.IO.Class (liftIO)
12−import qualified Data.Binary as Binary
1310import Data.Bool (bool)
14−import qualified Data.ByteString.Base16 as Base16
15−import qualified Data.ByteString.Lazy as LBS
11+import Data.Foldable (for_)
1612import Data.List
17−import Data.Maybe (catMaybes)
1813import qualified Data.Text as T
19−import Data.Text.Encoding
20−import GHC.StaticPtr
2114import Options.Applicative
15+import Options.Applicative.Help.Pretty (text, (<$$>))
2216import System.Directory
2317import System.Environment
2418import System.FilePath
@@ -28,13 +22,12 @@ import System.Posix.Process (executeFile)
2822
2923import Obelisk.App
3024import Obelisk.CliApp
31−import Obelisk.CliApp.Demo (cliDemo)
3225import Obelisk.Command.Deploy
3326import Obelisk.Command.Project
3427import Obelisk.Command.Run
3528import Obelisk.Command.Thunk
36−import Obelisk.Command.Utils
3729import qualified Obelisk.Command.VmBuilder as VmBuilder
30+import qualified Obelisk.Command.Preprocessor as Preprocessor
3831
3932
4033data Args = Args
@@ -88,50 +81,43 @@ data ObCommand
8881 = ObCommand_Init InitSource Bool
8982 | ObCommand_Deploy DeployCommand
9083 | ObCommand_Run
84+ | ObCommand_Profile String [String]
9185 | ObCommand_Thunk ThunkCommand
9286 | ObCommand_Repl
9387 | ObCommand_Watch
88+ | ObCommand_Shell ShellOpts
89+ | ObCommand_Doc String [String] -- shell and list of packages
90+ | ObCommand_Hoogle String Int -- shell and port
9491 | ObCommand_Internal ObInternal
9592 deriving Show
9693
9794data ObInternal
98− = ObInternal_RunStaticIO StaticKey
99− | ObInternal_CLIDemo
95+ -- the preprocessor argument syntax is also handled outside
96+ -- optparse-applicative, but it shouldn't ever conflict with another syntax
97+ = ObInternal_ApplyPackages String String String [String]
10098 deriving Show
10199
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−
120100obCommand :: ArgsConfig -> Parser ObCommand
121101obCommand cfg = hsubparser
122− (mconcat
123− [ 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"
125− , command "run" $ info (pure ObCommand_Run) $ progDesc "Run current project in development mode"
126− , command "thunk" $ info (ObCommand_Thunk <$> thunkCommand) $ progDesc "Manipulate thunk directories"
127− , 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"
129− ])
130− <|> subparser
131− (mconcat
132− [ internal
133− , command "internal" (info (ObCommand_Internal <$> internalCommand) mempty)
134− ])
102+ (mconcat
103+ [ command "init" $ info (ObCommand_Init <$> initSource <*> initForce) $ progDesc "Initialize an Obelisk project"
104+ , command "deploy" $ info (ObCommand_Deploy <$> deployCommand cfg) $ progDesc "Prepare a deployment for an Obelisk project"
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"
107+ , command "thunk" $ info (ObCommand_Thunk <$> thunkCommand) $ progDesc "Manipulate thunk directories"
108+ , command "repl" $ info (pure ObCommand_Repl) $ progDesc "Open an interactive interpreter"
109+ , command "watch" $ info (pure ObCommand_Watch) $ progDesc "Watch current project for errors and warnings"
110+ , command "shell" $ info (ObCommand_Shell <$> shellOpts) $ progDesc "Enter a shell with project dependencies"
111+ , command "doc" $ info (ObCommand_Doc <$> shellFlags <*> packageNames) $
112+ progDesc "List paths to haddock documentation for specified packages"
113+ <> footerDoc (Just $
114+ text "Hint: To open the documentation you can pipe the output of this command like"
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..."))
135121
136122deployCommand :: ArgsConfig -> Parser DeployCommand
137123deployCommand cfg = hsubparser $ mconcat
@@ -143,7 +129,7 @@ deployCommand cfg = hsubparser $ mconcat
143129 where
144130 platformP = hsubparser $ mconcat
145131 [ 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
147133 ]
148134
149135 remoteBuilderParser :: Parser (Maybe RemoteBuilder)
@@ -165,8 +151,8 @@ deployCommand cfg = hsubparser $ mconcat
165151
166152deployInitOpts :: Parser DeployInitOpts
167153deployInitOpts = DeployInitOpts
168− <$> strArgument (action "directory" <> metavar "DEPLOYDIR" <> help "Path to a directory that it will create")
169− <*> strOption (long "ssh-key" <> action "file" <> metavar "SSHKEY" <> help "Path to an ssh key that it will symlink to")
154+ <$> strArgument (action "directory" <> metavar "DEPLOYDIR" <> help "Path to a directory where the deployment repository will be initialized")
155+ <*> strOption (long "ssh-key" <> action "file" <> metavar "SSHKEY" <> help "Path to an SSH key that will be *copied* to the deployment repository")
170156 <*> some (strOption (long "hostname" <> metavar "HOSTNAME" <> help "hostname of the deployment target"))
171157 <*> strOption (long "route" <> metavar "PUBLICROUTE" <> help "Publicly accessible URL of your app")
172158 <*> strOption (long "admin-email" <> metavar "ADMINEMAIL" <> help "Email address where administrative alerts will be sent")
@@ -193,11 +179,24 @@ data DeployInitOpts = DeployInitOpts
193179 }
194180 deriving Show
195181
196−internalCommand :: Parser ObInternal
197−internalCommand = subparser $ mconcat
198− [ command "run-static-io" $ info (ObInternal_RunStaticIO <$> argument (eitherReader decodeStaticKey) (action "static-key")) mempty
199− , command "clidemo" $ info (pure ObInternal_CLIDemo) mempty
200− ]
182+profileCommand :: Parser (String, [String])
183+profileCommand = (,)
184+ <$> strOption
185+ ( long "output"
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+
201200
202201--TODO: Result should provide normalised path and also original user input for error reporting.
203202thunkDirectoryParser :: Parser FilePath
@@ -207,33 +206,70 @@ thunkDirectoryParser = fmap (dropTrailingPathSeparator . normalise) . strArgumen
207206 , help "Path to directory containing thunk data"
208207 ]
209208
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+
210227data ThunkCommand
211− = ThunkCommand_Update [FilePath] (Maybe String)
228+ = ThunkCommand_Update [FilePath] ThunkUpdateConfig
212229 | ThunkCommand_Unpack [FilePath]
213− | ThunkCommand_Pack [FilePath]
230+ | ThunkCommand_Pack [FilePath] ThunkPackConfig
214231 deriving Show
215232
216233thunkCommand :: Parser ThunkCommand
217234thunkCommand = 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"
219236 , 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"
221238 ]
222239
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+
223261parserPrefs :: ParserPrefs
224262parserPrefs = defaultPrefs
225263 { prefShowHelpOnEmpty = True
226264 }
227265
228−parseCLIArgs :: ArgsConfig -> [String] -> IO Args
229−parseCLIArgs cfg as = pure as >>= handleParseResult . execParserPure parserPrefs (argsInfo cfg)
230−
231266-- | Create an Obelisk config for the current process.
232267mkObeliskConfig :: IO Obelisk
233268mkObeliskConfig = do
234269 cliArgs <- getArgs
235270 -- This function should not use argument parser (full argument parsing happens post handoff)
236− let logLevel = toLogLevel $ "-v" `elem` cliArgs
271+ -- TODO: See if we can use the argument parser with a subset of the parsers to get logging level out.
272+ let logLevel = toLogLevel $ any (`elem` ["-v", "--verbose"]) cliArgs
237273 notInteractive <- not <$> isInteractiveTerm
238274 cliConf <- newCliConfig logLevel notInteractive notInteractive $ \case
239275 ObeliskError_ProcessError (ProcessFailure p code) ann ->
@@ -298,6 +334,9 @@ main' argsCfg = do
298334 liftIO $ executeFile impl False ("--no-handoff" : myArgs) Nothing
299335 case myArgs of
300336 "--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
301340 a:as -- Otherwise bash completion would always hand-off even if the user isn't trying to
302341 | "--bash-completion" `isPrefixOf` a
303342 && "--no-handoff" `elem` as -> go (a:as)
@@ -324,14 +363,13 @@ ob = \case
324363 Right (ThunkData_Packed ptr) -> return ptr
325364 Right (ThunkData_Checkout (Just ptr)) -> return ptr
326365 Right (ThunkData_Checkout Nothing) ->
327− getThunkPtr' False root
366+ getThunkPtr False root Nothing
328367 let sshKeyPath = _deployInitOpts_sshKey deployOpts
329368 hostname = _deployInitOpts_hostname deployOpts
330369 route = _deployInitOpts_route deployOpts
331370 adminEmail = _deployInitOpts_adminEmail deployOpts
332371 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
335373 DeployCommand_Push remoteBuilder -> do
336374 deployPath <- liftIO $ canonicalizePath "."
337375 deployPush deployPath $ case remoteBuilder of
@@ -339,35 +377,33 @@ ob = \case
339377 Just RemoteBuilder_ObeliskVM -> (:[]) <$> VmBuilder.getNixBuildersArg
340378 DeployCommand_Update -> deployUpdate "."
341379 DeployCommand_Test (platform, extraArgs) -> deployMobile platform extraArgs
342− ObCommand_Run -> inNixShell' $ static run
343− -- inNixShell ($(mkClosure 'ghcidAction) ())
380+ ObCommand_Run -> run
381+ ObCommand_Profile basePath rtsFlags -> profile basePath rtsFlags
344382 ObCommand_Thunk tc -> case tc of
345− ThunkCommand_Update thunks mBranch -> mapM_ ((flip updateThunkToLatest) mBranch) thunks
346− ThunkCommand_Unpack thunks -> mapM_ unpackThunk thunks
347− ThunkCommand_Pack thunks -> forM_ thunks packThunk
383+ ThunkCommand_Update thunks config -> for_ thunks (updateThunkToLatest config)
384+ ThunkCommand_Unpack thunks -> for_ thunks unpackThunk
385+ ThunkCommand_Pack thunks config -> for_ thunks (packThunk config)
348386 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"
350394 ObCommand_Internal icmd -> case icmd of
351− ObInternal_RunStaticIO k -> liftIO (unsafeLookupStaticPtr @(ObeliskT IO ()) k) >>= \case
352− Nothing -> failWith $ "ObInternal_RunStaticIO: no such StaticKey: " <> T.pack (show k)
353− Just p -> do
354− c <- getObelisk
355− liftIO $ runObelisk c $ deRefStaticPtr p
356− ObInternal_CLIDemo -> cliDemo
395+ ObInternal_ApplyPackages origPath inPath outPath packagePaths -> do
396+ liftIO $ Preprocessor.applyPackages origPath inPath outPath packagePaths
397+
398+haddockCommand :: [String] -> String
399+haddockCommand pkgs = unwords
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)"
357405
358406--TODO: Clean up all the magic strings throughout this codebase
359407
360408getArgsConfig :: IO ArgsConfig
361409getArgsConfig = 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 @@
44{-# LANGUAGE ScopedTypeVariables #-}
55{-# LANGUAGE ViewPatterns #-}
66{-# LANGUAGE DeriveGeneric #-}
7−{-# LANGUAGE TypeApplications #-}
87module Obelisk.Command.Deploy where
98
109import Control.Lens
1110import Control.Monad
12−import Control.Monad.Catch (Exception (displayException), MonadThrow, throwM, try, bracket_)
11+import Control.Monad.Catch (Exception (displayException), MonadThrow, bracket, throwM, try)
1312import Control.Monad.IO.Class (liftIO)
1413import Data.Aeson (FromJSON, ToJSON, encode, eitherDecode)
1514import Data.Bits
1615import qualified Data.ByteString.Lazy as BSL
1716import Data.Default
18−import qualified Data.HashMap.Strict as HM
1917import qualified Data.Map as Map
20−import Data.Text (Text)
2118import qualified Data.Set as Set
2219import qualified Data.Text as T
2320import qualified Data.Text.IO as T
2421import GHC.Generics
25−import Nix.Convert
26−import Nix.Pretty
27−import Nix.Value
2822import System.Directory
29−import System.Environment (getEnvironment)
3023import System.FilePath
3124import System.IO
3225import System.Posix.Files
33−import System.Process (delegate_ctlc, env, proc, cwd)
3426import Text.URI (URI)
3527import qualified Text.URI as URI
3628import Text.URI.Lens
3729
3830import Obelisk.App (MonadObelisk)
39−import Obelisk.CliApp (Severity (..), callProcessAndLogOutput, failWith, putLog, withSpinner)
31+import Obelisk.CliApp (
32+ Severity (..), callProcessAndLogOutput, failWith, proc, putLog,
33+ setCwd, setDelegateCtlc, setEnvOverride, withSpinner)
4034import Obelisk.Command.Nix
4135import Obelisk.Command.Project
4236import Obelisk.Command.Thunk
4337import Obelisk.Command.Utils
4438
39+
4540deployInit
4641 :: MonadObelisk m
4742 => ThunkPtr
4843 -> FilePath
4944 -> FilePath
50− -> FilePath
5145 -> [String] -- ^ hostnames
5246 -> String -- ^ route
5347 -> String -- ^ admin email
5448 -> Bool -- ^ enable https
5549 -> m ()
56−deployInit thunkPtr configDir deployDir sshKeyPath hostnames route adminEmail enableHttps = do
57− (hasConfigDir, localKey) <- withSpinner ("Preparing " <> T.pack deployDir) $ do
58− hasConfigDir <- liftIO $ do
59− createDirectoryIfMissing True deployDir
60− doesDirectoryExist configDir
50+deployInit thunkPtr deployDir sshKeyPath hostnames route adminEmail enableHttps = do
51+ liftIO $ createDirectoryIfMissing True deployDir
52+ localKey <- withSpinner ("Preparing " <> T.pack deployDir) $ do
6153 localKey <- liftIO (doesFileExist sshKeyPath) >>= \case
6254 False -> failWith $ T.pack $ "ob deploy init: file does not exist: " <> sshKeyPath
6355 True -> pure $ deployDir </> "ssh_key"
6456 callProcessAndLogOutput (Notice, Error) $
65− proc "cp" [sshKeyPath, localKey]
57+ proc cp [sshKeyPath, localKey]
6658 liftIO $ setFileMode localKey $ ownerReadMode .|. ownerWriteMode
67− return $ (hasConfigDir, localKey)
59+ return localKey
6860 withSpinner "Validating configuration" $ do
6961 void $ getHostFromRoute enableHttps route -- make sure that hostname is present
7062 forM_ hostnames $ \hostname -> do
7163 putLog Notice $ "Verifying host keys (" <> T.pack hostname <> ")"
7264 -- Note: we can't use a spinner here as this function will prompt the user.
7365 verifyHostKey (deployDir </> "backend_known_hosts") localKey hostname
74− when hasConfigDir $ withSpinner "Importing project configuration" $ do
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.
71+ withSpinner "Creating project configuration directories" $ do
7572 callProcessAndLogOutput (Notice, Error) $
76− proc "cp" [ "-r" , "-T" , configDir , deployDir </> "config"]
73+ proc "mkdir" [ "-p"
74+ , deployDir </> "config" </> "backend"
75+ , deployDir </> "config" </> "common"
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+
7784 withSpinner "Writing deployment configuration" $ do
7885 writeDeployConfig deployDir "backend_hosts" $ unlines hostnames
7986 writeDeployConfig deployDir "enable_https" $ show enableHttps
8087 writeDeployConfig deployDir "admin_email" adminEmail
81− writeDeployConfig deployDir ("config" </> "common" </> "route") $ route
82− withSpinner "Creating source thunk (./src)" $ liftIO $ do
83− createThunk (deployDir </> "src") thunkPtr
84− setupObeliskImpl deployDir
88+ writeDeployConfig deployDir ("config" </> "common" </> "route") route
89+ writeDeployConfig deployDir "module.nix" $
90+ "(import " <> toNixPath (makeRelative deployDir srcDir) <> " {}).obelisk.serverModules.mkBaseEc2"
91+
8592 withSpinner ("Initializing git repository (" <> T.pack deployDir <> ")") $
8693 initGit deployDir
8794
8895setupObeliskImpl :: FilePath -> IO ()
8996setupObeliskImpl deployDir = do
90− let implDir = toImplDir deployDir
97+ let
98+ implDir = toImplDir deployDir
99+ goBackUp = foldr (</>) "" $ (".." <$) $ splitPath $ makeRelative deployDir implDir
91100 createDirectoryIfMissing True implDir
92− writeFile (implDir </> "default.nix") "(import ../../src {}).obelisk"
101+ writeFile (implDir </> "default.nix") $ "(import " <> toNixPath (goBackUp </> "src") <> " {}).obelisk"
93102
94103deployPush :: MonadObelisk m => FilePath -> m [String] -> m ()
95104deployPush deployPath getNixBuilders = do
@@ -103,11 +112,13 @@ deployPush deployPath getNixBuilders = do
103112 Right (ThunkData_Packed ptr) -> return ptr
104113 Right (ThunkData_Checkout _) -> do
105114 checkGitCleanStatus srcPath True >>= \case
106− True -> packThunk srcPath
115+ True -> packThunk (ThunkPackConfig False (ThunkConfig Nothing)) srcPath
107116 False -> failWith $ T.pack $ "ob deploy push: ensure " <> srcPath <> " has no pending changes and latest is pushed upstream."
108117 Left err -> failWith $ "ob deploy push: couldn't read src thunk: " <> T.pack (show err)
109118 let version = show . _thunkRev_commit $ _thunkPtr_rev thunkPtr
110119 builders <- getNixBuilders
120+ let moduleFile = deployPath </> "module.nix"
121+ moduleFileExists <- liftIO $ doesFileExist moduleFile
111122 buildOutputByHost <- ifor (Map.fromSet (const ()) hosts) $ \host () -> do
112123 --TODO: What does it mean if this returns more or less than 1 line of output?
113124 [result] <- fmap lines $ nixCmd $ NixCmd_Build $ def
@@ -117,13 +128,13 @@ deployPush deployPath getNixBuilders = do
117128 , _target_expr = Nothing
118129 }
119130 & nixBuildConfig_outLink .~ OutLink_None
120− & nixCmdConfig_args .~
131+ & nixCmdConfig_args .~ (
121132 [ strArg "hostName" host
122133 , strArg "adminEmail" adminEmail
123134 , strArg "routeHost" routeHost
124135 , strArg "version" version
125136 , boolArg "enableHttps" enableHttps
126− ]
137+ ] <> [rawArg "module" ("import " <> toNixPath moduleFile) | moduleFileExists ])
127138 & nixCmdConfig_builders .~ builders
128139 pure result
129140 let knownHostsPath = deployPath </> "backend_known_hosts"
@@ -131,7 +142,7 @@ deployPush deployPath getNixBuilders = do
131142 withSpinner "Uploading closures" $ ifor_ buildOutputByHost $ \host outputPath -> do
132143 callProcess'
133144 (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]
135146 withSpinner "Uploading config" $ ifor_ buildOutputByHost $ \host _ -> do
136147 callProcessAndLogOutput (Notice, Warning) $
137148 proc "rsync"
@@ -153,7 +164,7 @@ deployPush deployPath getNixBuilders = do
153164 ]
154165 isClean <- checkGitCleanStatus deployPath True
155166 when (not isClean) $ do
156− withSpinner "Commiting changes to Git" $ do
167+ withSpinner "Committing changes to Git" $ do
157168 callProcessAndLogOutput (Debug, Error) $ proc "git"
158169 ["-C", deployPath, "add", "."]
159170 callProcessAndLogOutput (Debug, Error) $ proc "git"
@@ -161,25 +172,11 @@ deployPush deployPath getNixBuilders = do
161172 putLog Notice $ "Deployed => " <> T.pack route
162173 where
163174 callProcess' envMap cmd args = do
164− processEnv <- Map.toList . (envMap <>) . Map.fromList <$> liftIO getEnvironment
165− let p = (proc cmd args) { delegate_ctlc = True, env = Just processEnv }
175+ let p = setEnvOverride (envMap <>) $ setDelegateCtlc True $ proc cmd args
166176 callProcessAndLogOutput (Notice, Notice) p
167177
168178deployUpdate :: MonadObelisk m => FilePath -> m ()
169−deployUpdate deployPath = updateThunkToLatest (deployPath </> "src") Nothing
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− ]
179+deployUpdate deployPath = updateThunkToLatest (ThunkUpdateConfig Nothing (ThunkConfig Nothing)) (deployPath </> "src")
183180
184181data PlatformDeployment = Android | IOS
185182 deriving (Show, Eq)
@@ -189,13 +186,13 @@ renderPlatformDeployment = \case
189186 Android -> "android"
190187 IOS -> "ios"
191188
192−deployMobile :: MonadObelisk m => PlatformDeployment -> [String] -> m ()
189+deployMobile :: forall m. MonadObelisk m => PlatformDeployment -> [String] -> m ()
193190deployMobile platform mobileArgs = withProjectRoot "." $ \root -> do
194191 let srcDir = root </> "src"
195192 configDir = root </> "config"
196193 exists <- liftIO $ doesDirectoryExist srcDir
197194 unless exists $ failWith "ob test should be run inside of a deploy directory"
198− nixBuildTarget <- case platform of
195+ (nixBuildTarget, extraArgs) <- case platform of
199196 Android -> do
200197 let keystorePath = root </> "android_keystore.jks"
201198 keytoolConfPath = root </> "android_keytool_config.json"
@@ -214,48 +211,60 @@ deployMobile platform mobileArgs = withProjectRoot "." $ \root -> do
214211 , _keytoolConfig_storepass = keyStorePassword
215212 , _keytoolConfig_keypass = keyStorePassword
216213 }
217− createKeystore root $ keyToolConf
214+ createKeystore root keyToolConf
218215 liftIO $ BSL.writeFile keytoolConfPath $ encode keyToolConf
219216 checkKeytoolConfExist <- liftIO $ doesFileExist keytoolConfPath
220217 unless checkKeytoolConfExist $ failWith "Missing android KeytoolConfig"
221218 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
224220 Left err -> failWith $ T.pack err
225− Right conf -> do
226− let nvset = toValue @(HM.HashMap Text (NValueNF Identity)) @Identity @(NValueNF Identity) $ keytoolToAndroidConfig conf
227− return $ printNix $ runIdentity nvset
221+ Right conf -> pure
222+ [ "--sign"
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+ ]
228228 let expr = mconcat
229− [ "with (import ", srcDir, " {});"
230− , "android.frontend.override (drv: { "
231− , "releaseKey = (if builtins.isNull drv.releaseKey then {} else drv.releaseKey) // " <> releaseKey <> "; "
232− , "staticSrc = (passthru.__android ", configDir, ").frontend.staticSrc;"
229+ [ "with (import ", toNixPath srcDir, " {});"
230+ , "android.frontend.override (drv: {"
231+ , "isRelease = true;"
232+ , "staticSrc = (passthru.__androidWithConfig ", configDir, ").frontend.staticSrc;"
233+ , "assets = (passthru.__androidWithConfig ", configDir, ").frontend.assets;"
233234 , "})"
234235 ]
235− return $ Target
236+ return (Target
236237 { _target_path = Nothing
237238 , _target_attr = Nothing
238239 , _target_expr = Just expr
239− }
240+ }, keyArgs)
240241 IOS -> do
241242 let expr = mconcat
242− [ "with (import ", srcDir, " {});"
243− , "ios.frontend.override (_: { staticSrc = (passthru.__ios ", configDir, ").frontend.staticSrc; })"
243+ [ "with (import ", toNixPath srcDir, " {});"
244+ , "ios.frontend.override (_: { staticSrc = (passthru.__iosWithConfig ", toNixPath configDir, ").frontend.staticSrc; })"
244245 ]
245− return $ Target
246+ return (Target
246247 { _target_path = Nothing
247248 , _target_attr = Nothing
248249 , _target_expr = Just expr
249− }
250+ }, [])
250251 result <- nixCmd $ NixCmd_Build $ def
251252 & nixBuildConfig_outLink .~ OutLink_None
252253 & nixCmdConfig_target .~ nixBuildTarget
253− putLog Notice $ T.pack $ "Your recently built android apk can be found at the following path: " <> (show result)
254− callProcessAndLogOutput (Notice, Error) $ proc (result </> "bin" </> "deploy") mobileArgs
254+ let mobileArtifact = case platform of
255+ IOS -> "iOS App"
256+ Android -> "Android APK"
257+ putLog Notice $ T.pack $ unwords ["Your recently built", mobileArtifact, "can be found at the following path:", show result]
258+ callProcessAndLogOutput (Notice, Error) $ proc (result </> "bin" </> "deploy") (mobileArgs ++ extraArgs)
255259 where
256− withEcho showEcho f = do
257− prevEcho <- hGetEcho stdin
258− bracket_ (hSetEcho stdin showEcho) (hSetEcho stdin prevEcho) f
260+ withEcho showEcho f = bracket
261+ (do
262+ prevEcho <- hGetEcho stdin
263+ hSetEcho stdin showEcho
264+ pure prevEcho
265+ )
266+ (hSetEcho stdin)
267+ (const f)
259268
260269data KeytoolConfig = KeytoolConfig
261270 { _keytoolConfig_keystore :: FilePath
@@ -268,19 +277,16 @@ instance FromJSON KeytoolConfig
268277instance ToJSON KeytoolConfig
269278
270279createKeystore :: MonadObelisk m => FilePath -> KeytoolConfig -> m ()
271−createKeystore root config = do
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 }
274− where
275− keytoolCmd = processToShellString "keytool"
276− [ "-genkeypair", "-noprompt"
277− , "-keystore", _keytoolConfig_keystore config
278− , "-keyalg", "RSA", "-keysize", "2048"
279− , "-validity", "1000000000"
280− , "-storepass", _keytoolConfig_storepass config
281− , "-alias", _keytoolConfig_alias config
282− , "-keypass", _keytoolConfig_keypass config
283− ]
280+createKeystore root config =
281+ callProcessAndLogOutput (Notice, Notice) $ setCwd (Just root) $ proc jreKeyToolPath
282+ [ "-genkeypair", "-noprompt"
283+ , "-keystore", _keytoolConfig_keystore config
284+ , "-keyalg", "RSA", "-keysize", "2048"
285+ , "-validity", "1000000"
286+ , "-storepass", _keytoolConfig_storepass config
287+ , "-alias", _keytoolConfig_alias config
288+ , "-keypass", _keytoolConfig_keypass config
289+ ]
284290
285291-- | Simplified deployment configuration mechanism. At one point we may revisit this.
286292writeDeployConfig :: MonadObelisk m => FilePath -> FilePath -> String -> m ()
lib/command/src/Obelisk/Command/Nix.hs+65−25
@@ -9,22 +9,33 @@
99{-# LANGUAGE TemplateHaskell #-}
1010{-# LANGUAGE TypeApplications #-}
1111module Obelisk.Command.Nix
12− ( nixCmd
13− , NixCmd (..)
14− , nixCmdConfig_target
15− , nixCmdConfig_args
16− , nixCmdConfig_builders
12+ ( Arg (..)
1713 , NixBuildConfig (..)
1814 , nixBuildConfig_common
1915 , nixBuildConfig_outLink
16+ , NixCmd (..)
17+ , nixCmdConfig_args
18+ , nixCmdConfig_builders
19+ , nixCmdConfig_target
20+ , NixCommonConfig (..)
2021 , NixInstantiateConfig (..)
2122 , nixInstantiateConfig_eval
22− , NixCommonConfig (..)
23− , Target (..)
23+ , NixShellConfig (..)
24+ , nixShellConfig_common
25+ , nixShellConfig_pure
26+ , nixShellConfig_run
2427 , OutLink (..)
25− , Arg (..)
28+ , Target (..)
29+ , target_attr
30+ , target_expr
31+ , target_path
32+
2633 , boolArg
34+ , nixCmd
35+ , nixCmdProc
36+ , nixCmdProc'
2737 , rawArg
38+ , runNixShellConfig
2839 , strArg
2940 ) where
3041
@@ -37,7 +48,6 @@ import Data.List (intercalate)
3748import Data.Maybe
3849import Data.Monoid ((<>))
3950import qualified Data.Text as T
40−import System.Process (proc)
4151
4252import Obelisk.App (MonadObelisk)
4353import Obelisk.CliApp
@@ -63,10 +73,10 @@ data Arg
6373 deriving (Eq, Show)
6474
6575strArg :: String -> String -> Arg
66−strArg k = Arg_Str k
76+strArg = Arg_Str
6777
6878rawArg :: String -> String -> Arg
69−rawArg k = Arg_Expr k
79+rawArg = Arg_Expr
7080
7181boolArg :: String -> Bool -> Arg
7282boolArg k = Arg_Expr k . bool "false" "true"
@@ -86,7 +96,7 @@ makeClassy ''NixCommonConfig
8696instance Default NixCommonConfig where
8797 def = NixCommonConfig def mempty mempty
8898
89−runNixCommonConfig :: NixCommonConfig -> [FilePath]
99+runNixCommonConfig :: NixCommonConfig -> [String]
90100runNixCommonConfig cfg = mconcat [maybeToList path, attrArg, exprArg, args, buildersArg]
91101 where
92102 path = _target_path $ _nixCmdConfig_target cfg
@@ -124,7 +134,7 @@ instance HasNixCommonConfig NixBuildConfig where
124134instance Default NixBuildConfig where
125135 def = NixBuildConfig def def
126136
127−runNixBuildConfig :: NixBuildConfig -> [FilePath]
137+runNixBuildConfig :: NixBuildConfig -> [String]
128138runNixBuildConfig cfg = mconcat
129139 [ runNixCommonConfig $ cfg ^. nixCommonConfig
130140 , case _nixBuildConfig_outLink cfg of
@@ -145,12 +155,26 @@ instance HasNixCommonConfig NixInstantiateConfig where
145155instance Default NixInstantiateConfig where
146156 def = NixInstantiateConfig def False
147157
148−runNixInstantiateConfig :: NixInstantiateConfig -> [FilePath]
158+runNixInstantiateConfig :: NixInstantiateConfig -> [String]
149159runNixInstantiateConfig cfg = mconcat
150160 [ runNixCommonConfig $ cfg ^. nixCommonConfig
151161 , "--eval" <$ guard (_nixInstantiateConfig_eval cfg)
152162 ]
153163
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+
154178data NixCmd
155179 = NixCmd_Build NixBuildConfig
156180 | NixCmd_Instantiate NixInstantiateConfig
@@ -158,27 +182,43 @@ data NixCmd
158182instance Default NixCmd where
159183 def = NixCmd_Build def
160184
161−nixCmd :: MonadObelisk m => NixCmd -> m FilePath
162−nixCmd cmdCfg = withSpinner' ("Running " <> cmd <> desc) (Just $ const $ "Built " <> desc) $ do
163− output <- readProcessAndLogStderr Debug $ proc (T.unpack cmd) $ options
164− -- Remove final newline that Nix appends
165− Just (outPath, '\n') <- pure $ T.unsnoc output
166− pure $ T.unpack outPath
185+runNixShellConfig :: NixShellConfig -> [String]
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)
197+nixCmdProc' cmdCfg = (proc (T.unpack cmd) options, cmd)
167198 where
168− (cmd, options, commonCfg) = case cmdCfg of
199+ (cmd, options) = case cmdCfg of
169200 NixCmd_Build cfg' ->
170201 ( "nix-build"
171202 , runNixBuildConfig cfg'
172− , cfg' ^. nixCommonConfig
173203 )
174204 NixCmd_Instantiate cfg' ->
175205 ( "nix-instantiate"
176206 , runNixInstantiateConfig cfg'
177− , cfg' ^. nixCommonConfig
178207 )
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
179220 path = commonCfg ^. nixCmdConfig_target . target_path
180221 desc = T.pack $ mconcat $ catMaybes
181− [ (" on " <>) <$> path
222+ [ ("on " <>) <$> path
182223 , (\a -> " [" <> a <> "]") <$> (commonCfg ^. nixCmdConfig_target . target_attr)
183224 ]
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 @@
33{-# LANGUAGE OverloadedStrings #-}
44module Obelisk.Command.Project
55 ( InitSource (..)
6− , initProject
76 , findProjectObeliskCommand
87 , findProjectRoot
9− , withProjectRoot
10− , inProjectShell
11− , inImpureProjectShell
8+ , findProjectAssets
9+ , initProject
10+ , nixShellRunConfig
11+ , nixShellRunProc
12+ , nixShellWithHoogle
13+ , nixShellWithPkgs
14+ , obeliskDirName
1215 , projectShell
13−
14− , toObeliskDir
1516 , toImplDir
17+ , toNixPath
18+ , toObeliskDir
19+ , withProjectRoot
1620 ) where
1721
22+import Control.Concurrent.MVar (MVar, newMVar, withMVarMasked)
23+import Control.Lens ((.~), (?~), (<&>))
1824import Control.Monad
1925import Control.Monad.IO.Class (liftIO)
2026import Control.Monad.State
27+import qualified Data.Aeson as Json
2128import Data.Bits
22−import Data.Function (on)
29+import qualified Data.ByteString.Lazy as BSL
30+import Data.Default (def)
31+import Data.Function ((&), on)
32+import Data.List (isInfixOf)
33+import Data.Map (Map)
34+import Data.Text (Text)
2335import qualified Data.Text as T
36+import Data.Text.Encoding (decodeUtf8)
37+import Data.Traversable (for)
2438import System.Directory
39+import System.Environment (lookupEnv)
2540import System.FilePath
2641import System.IO.Temp
27−import System.Posix (FileStatus, UserID, deviceID, fileID, fileMode, fileOwner, getFileStatus, getRealUserID)
42+import System.IO.Unsafe (unsafePerformIO)
43+import System.Posix (FileStatus, FileMode, CMode (..), UserID, deviceID, fileID, fileMode, fileOwner, getFileStatus, getRealUserID)
2844import System.Posix.Files
29−import System.Process (CreateProcess, cwd, proc, waitForProcess, delegate_ctlc)
3045
3146import GitHub.Data.GitData (Branch)
3247import GitHub.Data.Name (Name)
3348
3449import Obelisk.App (MonadObelisk)
3550import Obelisk.CliApp
51+import Obelisk.Command.Nix
3652import Obelisk.Command.Thunk
53+import Obelisk.Command.Utils (nixExePath, nixBuildExePath)
54+
3755--TODO: Make this module resilient to random exceptions
3856
3957--TODO: Don't hardcode this
@@ -47,17 +65,21 @@ obeliskSourceWithBranch branch = ThunkSource_GitHub $ GitHubSource
4765 { _gitHubSource_owner = "obsidiansystems"
4866 , _gitHubSource_repo = "obelisk"
4967 , _gitHubSource_branch = Just branch
68+ , _gitHubSource_private = False
5069 }
5170
5271data InitSource
53− = InitSource_Default
54− | InitSource_Branch (Name Branch)
55− | InitSource_Symlink FilePath
56− deriving Show
72+ = InitSource_Default
73+ | InitSource_Branch (Name Branch)
74+ | InitSource_Symlink FilePath
75+ deriving Show
76+
77+obeliskDirName :: FilePath
78+obeliskDirName = ".obelisk"
5779
5880-- | Path to obelisk directory in given path
5981toObeliskDir :: FilePath -> FilePath
60−toObeliskDir p = p </> ".obelisk"
82+toObeliskDir p = p </> obeliskDirName
6183
6284-- | Path to impl file in given path
6385toImplDir :: FilePath -> FilePath
@@ -114,15 +136,16 @@ initProject source force = withSystemTempDirectory "ob-init" $ \tmpDir -> do
114136findProjectObeliskCommand :: MonadObelisk m => FilePath -> m (Maybe FilePath)
115137findProjectObeliskCommand target = do
116138 myUid <- liftIO getRealUserID
139+ processUmask <- liftIO getUmask
117140 targetStat <- liftIO $ getFileStatus target
118− (result, insecurePaths) <- flip runStateT [] $ walkToProjectRoot target targetStat myUid >>= \case
141+ (result, insecurePaths) <- flip runStateT [] $ walkToProjectRoot target targetStat processUmask myUid >>= \case
119142 Nothing -> pure Nothing
120143 Just projectRoot -> liftIO (doesDirectoryExist $ toImplDir projectRoot) >>= \case
121144 False -> do
122145 putLog Warning $ "Found obelisk directory in " <> T.pack projectRoot <> " but the implementation (impl) file is missing"
123146 pure Nothing
124147 True -> do
125− walkToImplDir projectRoot myUid -- For security check
148+ walkToImplDir projectRoot myUid processUmask -- For security check
126149 return $ Just projectRoot
127150 case (result, insecurePaths) of
128151 (Just projDir, []) -> do
@@ -133,17 +156,37 @@ findProjectObeliskCommand target = do
133156 putLog Error $ T.unlines
134157 [ "Error: Found a project at " <> T.pack (normalise projDir) <> ", but had to traverse one or more insecure directories to get there:"
135158 , T.unlines $ fmap (T.pack . normalise) insecurePaths
136− , "Please ensure that all of these directories are owned by you and are not writable by anyone else."
159+ , "Please ensure that all of these directories are owned by you, not world-writable, and no more group-writable than permitted by your umask."
137160 ]
138161 return Nothing
139162
163+-- | Get the umask for the Obelisk process.
164+--
165+-- Because of
166+-- http://man7.org/linux/man-pages/man2/umask.2.html#NOTES we have to set the
167+-- umask to read it. We are using 'withMVarMasked' to guarantee that setting and
168+-- reading isn't interrupted by any exception or interleaved with another thread.
169+getUmask :: IO FileMode
170+getUmask = withMVarMasked globalUmaskLock $ \() -> do
171+ initialMask <- setFileCreationMask safeUmask
172+ void (setFileCreationMask initialMask)
173+ pure initialMask
174+ where
175+ safeUmask :: FileMode
176+ safeUmask = CMode 0o22
177+
178+{-# NOINLINE globalUmaskLock #-}
179+globalUmaskLock :: MVar ()
180+globalUmaskLock = unsafePerformIO (newMVar ())
181+
140182-- | Get the FilePath to the containing project directory, if there is one
141183findProjectRoot :: MonadObelisk m => FilePath -> m (Maybe FilePath)
142184findProjectRoot target = do
143185 myUid <- liftIO getRealUserID
144186 targetStat <- liftIO $ getFileStatus target
145− (result, _) <- liftIO $ runStateT (walkToProjectRoot target targetStat myUid) []
146− return result
187+ umask <- liftIO getUmask
188+ (result, _) <- liftIO $ runStateT (walkToProjectRoot target targetStat umask myUid) []
189+ return $ makeRelative "." <$> result
147190
148191withProjectRoot :: MonadObelisk m => FilePath -> (FilePath -> m a) -> m a
149192withProjectRoot target f = findProjectRoot target >>= \case
@@ -155,15 +198,15 @@ withProjectRoot target f = findProjectRoot target >>= \case
155198-- traversed in the process. Return the project root directory, if found.
156199walkToProjectRoot
157200 :: (MonadState [FilePath] m, MonadIO m)
158− => FilePath -> FileStatus -> UserID -> m (Maybe FilePath)
159−walkToProjectRoot this thisStat myUid = liftIO (doesDirectoryExist this) >>= \case
201+ => FilePath -> FileStatus -> FileMode -> UserID -> m (Maybe FilePath)
202+walkToProjectRoot this thisStat desiredUmask myUid = liftIO (doesDirectoryExist this) >>= \case
160203 -- It's not a directory, so it can't be a project
161204 False -> do
162205 let dir = takeDirectory this
163206 dirStat <- liftIO $ getFileStatus dir
164− walkToProjectRoot dir dirStat myUid
207+ walkToProjectRoot dir dirStat desiredUmask myUid
165208 True -> do
166− when (not $ isWritableOnlyBy thisStat myUid) $ modify (this:)
209+ when (not $ isWellOwnedAndWellPermissioned thisStat myUid desiredUmask) $ modify (this:)
167210 liftIO (doesDirectoryExist $ toObeliskDir this) >>= \case
168211 True -> return $ Just this
169212 False -> do
@@ -173,47 +216,114 @@ walkToProjectRoot this thisStat myUid = liftIO (doesDirectoryExist this) >>= \ca
173216 isSameFileAs = (==) `on` fileIdentity
174217 if thisStat `isSameFileAs` nextStat
175218 then return Nothing -- Found a cycle; probably hit root directory
176− else walkToProjectRoot next nextStat myUid
219+ else walkToProjectRoot next nextStat desiredUmask myUid
177220
178221-- | Walk from the given project root directory to its Obelisk implementation
179222-- directory, accumulating potentially insecure directories that were traversed
180223-- in the process.
181−walkToImplDir :: (MonadState [FilePath] m, MonadIO m) => FilePath -> UserID -> m ()
182−walkToImplDir projectRoot myUid = do
224+walkToImplDir :: (MonadState [FilePath] m, MonadIO m) => FilePath -> UserID -> FileMode -> m ()
225+walkToImplDir projectRoot myUid umask = do
183226 let obDir = toObeliskDir projectRoot
184227 obDirStat <- liftIO $ getFileStatus obDir
185− when (not $ isWritableOnlyBy obDirStat myUid) $ modify (obDir:)
228+ when (not $ isWellOwnedAndWellPermissioned obDirStat myUid umask) $ modify (obDir:)
186229 let implThunk = obDir </> "impl"
187230 implThunkStat <- liftIO $ getFileStatus implThunk
188− when (not $ isWritableOnlyBy implThunkStat myUid) $ modify (implThunk:)
231+ when (not $ isWellOwnedAndWellPermissioned implThunkStat myUid umask) $ modify (implThunk:)
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
189242
190−--TODO: Is there a better way to ask if anyone else can write things?
191−--E.g. what about ACLs?
192−-- | Check to see if directory is only writable by a user whose User ID matches the second argument provided
193−isWritableOnlyBy :: FileStatus -> UserID -> Bool
194−isWritableOnlyBy s uid = fileOwner s == uid && fileMode s .&. 0o22 == 0
243+-- | Check to see if a directory respect the umask, but check explicitly that
244+-- it's not world writable in any case.
245+filePermissionIsSafe :: FileStatus -> FileMode -> Bool
246+filePermissionIsSafe s umask = not fileWorldWritable && fileGroupWritable <= umaskGroupWritable
247+ where
248+ fileWorldWritable = fileMode s .&. 0o002 == 0o002
249+ fileGroupWritable = fileMode s .&. 0o020 == 0o020
250+ umaskGroupWritable = umask .&. 0o020 == 0
195251
196−-- | Run a command in the given shell for the current project
197−inProjectShell :: MonadObelisk m => String -> String -> m ()
198−inProjectShell shellName command = withProjectRoot "." $ \root ->
199− projectShell root True shellName command
252+-- | Nix syntax requires relative paths to be prefixed by @./@ or
253+-- @../@. This will make a 'FilePath' that can be embedded in a Nix
254+-- expression.
255+toNixPath :: FilePath -> FilePath
256+toNixPath root | "/" `isInfixOf` root = root
257+ | otherwise = "./" <> root
200258
201−inImpureProjectShell :: MonadObelisk m => String -> String -> m ()
202−inImpureProjectShell shellName command = withProjectRoot "." $ \root ->
203− projectShell root False shellName command
259+nixShellRunConfig :: MonadObelisk m => FilePath -> Bool -> Maybe String -> m NixShellConfig
260+nixShellRunConfig root isPure command = do
261+ nixpkgsPath <- fmap T.strip $ readProcessAndLogStderr Debug $ setCwd (Just root) $
262+ proc nixExePath ["eval", "(import .obelisk/impl {}).nixpkgs.path"]
263+ nixRemote <- liftIO $ lookupEnv "NIX_REMOTE"
264+ pure $ def
265+ & nixShellConfig_pure .~ isPure
266+ & nixShellConfig_common . nixCmdConfig_target .~ (def & target_path .~ Nothing)
267+ & nixShellConfig_run .~ (command <&> \c -> mconcat
268+ [ "export NIX_PATH=nixpkgs=", T.unpack nixpkgsPath, "; "
269+ , maybe "" (\v -> "export NIX_REMOTE=" <> v <> "; ") nixRemote
270+ , c
271+ ])
204272
205−projectShell :: MonadObelisk m => FilePath -> Bool -> String -> String -> m ()
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+
303+projectShell :: MonadObelisk m => FilePath -> Bool -> String -> Maybe String -> m ()
206304projectShell root isPure shellName command = do
207− (_, _, _, ph) <- createProcess_ "runNixShellAttr" $ setCtlc $ setCwd (Just root) $ proc "nix-shell" $
208− [ "--pure" | isPure ] <>
209− [ "-A"
210− , "shells." <> shellName
211− , "--run", command
212− ]
213− void $ liftIO $ waitForProcess ph
214−
215−setCtlc :: CreateProcess -> CreateProcess
216−setCtlc cfg = cfg { delegate_ctlc = True }
217−
218−setCwd :: Maybe FilePath -> CreateProcess -> CreateProcess
219−setCwd fp cfg = cfg { cwd = fp }
305+ defShellConfig <- nixShellRunConfig root isPure command
306+ (_, _, _, ph) <- createProcess_ "runNixShellAttr" $ setCwd (Just root) $ nixShellRunProc $ defShellConfig
307+ & nixShellConfig_common . nixCmdConfig_target . target_path ?~ "default.nix"
308+ & nixShellConfig_common . nixCmdConfig_target . target_attr ?~ ("shells." <> shellName)
309+ void $ waitForProcess ph
310+
311+findProjectAssets :: MonadObelisk m => FilePath -> m Text
312+findProjectAssets root = do
313+ isDerivation <- readProcessAndLogStderr Debug $ setCwd (Just root) $
314+ proc nixExePath
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 @@
33{-# LANGUAGE MultiParamTypeClasses #-}
44{-# LANGUAGE OverloadedStrings #-}
55{-# LANGUAGE ScopedTypeVariables #-}
6+{-# LANGUAGE TupleSections #-}
7+{-# LANGUAGE QuasiQuotes #-}
68module Obelisk.Command.Run where
79
8−import Control.Exception (bracket)
9−import Control.Monad
10+import Control.Arrow ((&&&))
11+import Control.Exception (Exception, bracket)
12+import Control.Lens (ifor, (.~), (&))
13+import Control.Monad (filterM, unless, void)
14+import Control.Monad.Except (runExceptT, throwError)
1015import Control.Monad.IO.Class (liftIO)
1116import Control.Monad.Reader (MonadIO)
17+import Data.Coerce (coerce)
18+import Data.Default (def)
1219import Data.Either
13−import Data.List
14−import Data.List.NonEmpty as NE
20+import Data.Foldable (for_, toList)
21+import qualified Data.List.NonEmpty as NE
22+import Data.Map (Map)
23+import qualified Data.Map as Map
1524import Data.Maybe
25+import qualified Data.Set as Set
26+import Data.Text (Text)
1627import 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(..))
1733import Distribution.PackageDescription.Parsec (parseGenericPackageDescription)
1834import Distribution.Parsec.ParseResult (runParseResult)
19−import Distribution.Pretty
35+import qualified Distribution.System as Dist
2036import Distribution.Types.BuildInfo
2137import Distribution.Types.CondTree
2238import Distribution.Types.GenericPackageDescription
2339import Distribution.Types.Library
2440import Distribution.Utils.Generic
25−import Hpack.Config
26−import Hpack.Render
27−import Hpack.Yaml
41+import qualified Distribution.Parsec.Common as Dist
42+import qualified Hpack.Config as Hpack
43+import qualified Hpack.Render as Hpack
44+import qualified Hpack.Yaml as Hpack
2845import Language.Haskell.Extension
2946import Network.Socket hiding (Debug)
3047import System.Directory
48+import System.Environment (getExecutablePath)
3149import System.FilePath
32−import System.Process (proc)
50+import qualified System.Info
3351import System.IO.Temp (withSystemTempDirectory)
34−import Data.ByteString (ByteString)
3552
36−import Obelisk.App (MonadObelisk, ObeliskT)
37−import Obelisk.CliApp
38− ( CliT (..), HasCliConfig, Severity (..)
39− , callCommand, failWith, getCliConfig, putLog
40− , readProcessAndLogStderr, runCli)
41−import Obelisk.Command.Project (inProjectShell, withProjectRoot)
53+import Obelisk.App (MonadObelisk)
54+import Obelisk.CliApp (
55+ Severity (..),
56+ createProcess_,
57+ failWith,
58+ proc,
59+ putLog,
60+ readCreateProcessWithExitCode,
61+ readProcessAndLogStderr,
62+ setCwd,
63+ setDelegateCtlc,
64+ waitForProcess,
65+ withSpinner,
66+ )
67+import Obelisk.Command.Nix
68+import Obelisk.Command.Project (nixShellWithPkgs, toImplDir, withProjectRoot, findProjectAssets)
69+import Obelisk.Command.Thunk (attrCacheFileName)
70+import Obelisk.Command.Utils (findExePath, ghcidExePath)
4271
4372data CabalPackageInfo = CabalPackageInfo
44− { _cabalPackageInfo_packageRoot :: FilePath
73+ { _cabalPackageInfo_packageFile :: FilePath
74+ , _cabalPackageInfo_packageName :: T.Text
75+ , _cabalPackageInfo_packageRoot :: FilePath
76+ , _cabalPackageInfo_buildable :: Bool
4577 , _cabalPackageInfo_sourceDirs :: NE.NonEmpty FilePath
4678 -- ^ List of hs src dirs of the library component
4779 , _cabalPackageInfo_defaultExtensions :: [Extension]
4880 -- ^ 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)
4985 }
5086
51−-- NOTE: `run` is not polymorphic like the rest because we use StaticPtr to invoke it.
52−run :: ObeliskT IO ()
53−run = do
54− pkgs <- getLocalPkgs
55− withGhciScript pkgs $ \dotGhciPath -> do
87+-- | Used to signal to obelisk that it's being invoked as a preprocessor
88+preprocessorIdentifier :: String
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
129+ pkgs <- fmap toList . parsePackagesOrFail =<< getLocalPkgs root
130+ withGhciScript pkgs root $ \dotGhciPath -> do
56131 freePort <- getFreePort
57− assets <- withProjectRoot "." $ \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"]
132+ assets <- findProjectAssets root
62133 putLog Debug $ "Assets impurely loaded from: " <> assets
63− runGhcid dotGhciPath $ Just $ unwords
64− [ "run"
134+ runGhcid root True dotGhciPath pkgs $ Just $ unwords
135+ [ "Obelisk.Run.run"
65136 , show freePort
66− , "(runServeAsset " ++ show assets ++ ")"
137+ , "(Obelisk.Run.runServeAsset " ++ show assets ++ ")"
67138 , "Backend.backend"
68139 , "Frontend.frontend"
69140 ]
70141
71142runRepl :: MonadObelisk m => m ()
72−runRepl = do
73− pkgs <- getLocalPkgs
74− withGhciScript pkgs $ \dotGhciPath -> do
75− runGhciRepl dotGhciPath
143+runRepl = withProjectRoot "." $ \root -> do
144+ pkgs <- fmap toList . parsePackagesOrFail =<< getLocalPkgs root
145+ withGhciScript pkgs "." $ \dotGhciPath ->
146+ runGhciRepl root pkgs dotGhciPath
76147
77148runWatch :: MonadObelisk m => m ()
78−runWatch = do
79− pkgs <- getLocalPkgs
80− withGhciScript pkgs $ \dotGhciPath -> runGhcid dotGhciPath Nothing
149+runWatch = withProjectRoot "." $ \root -> do
150+ pkgs <- fmap toList . parsePackagesOrFail =<< getLocalPkgs root
151+ withGhciScript pkgs root $ \dotGhciPath ->
152+ runGhcid root True dotGhciPath pkgs Nothing
81153
82−-- | Relative paths to local packages of an obelisk project
83−-- TODO a way to query this
84−getLocalPkgs :: Applicative f => f [FilePath]
85−getLocalPkgs = pure ["backend", "common", "frontend"]
154+-- | Relative paths to local packages of an obelisk project.
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.
159+getLocalPkgs :: MonadObelisk m => FilePath -> m [FilePath]
160+getLocalPkgs root = do
161+ obeliskPaths <- runFind ["-L", root, "-name", ".obelisk", "-type", "d"]
86162
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.
87216parseCabalPackage
88− :: (MonadObelisk m)
89− => FilePath -- ^ package directory
217+ :: MonadObelisk m
218+ => FilePath -- ^ Package directory
90219 -> m (Maybe CabalPackageInfo)
91−parseCabalPackage dir = do
92− let cabalFp = dir </> (takeBaseName dir <> ".cabal")
93− hpackFp = dir </> "package.yaml"
94− hasCabal <- liftIO $ doesFileExist cabalFp
95− hasHpack <- liftIO $ doesFileExist hpackFp
96−
97− mCabalContents <- if hasCabal
98− then Just <$> liftIO (readUTF8File cabalFp)
99− else if hasHpack
100− then do
101− let decodeOptions = DecodeOptions hpackFp Nothing decodeYaml
102− liftIO (readPackageConfig decodeOptions) >>= \case
103− Left err -> do
104− putLog Error $ T.pack $ "Failed to parse " <> hpackFp <> ": " <> err
105− return Nothing
106− Right (DecodeResult hpackPackage _ _) -> do
107− return $ Just $ renderPackage [] hpackPackage
108− else return Nothing
109−
110− fmap join $ forM mCabalContents $ \cabalContents -> do
111− let (warnings, result) = runParseResult $ parseGenericPackageDescription $
112− toUTF8BS $ cabalContents
113− mapM_ (putLog Warning) $ fmap (T.pack . show) warnings
114− case result of
115− Right gpkg -> do
116− return $ do
117− (_, lib) <- simplifyCondTree (const $ pure True) <$> condLibrary gpkg
118− pure $ CabalPackageInfo
119− { _cabalPackageInfo_packageRoot = takeDirectory cabalFp
120− , _cabalPackageInfo_sourceDirs =
121− fromMaybe (pure ".") $ NE.nonEmpty $ hsSourceDirs $ libBuildInfo lib
122− , _cabalPackageInfo_defaultExtensions =
123− defaultExtensions $ libBuildInfo lib
124− }
125− Left (_, errors) -> do
126− putLog Error $ T.pack $ "Failed to parse " <> cabalFp <> ":"
127− mapM_ (putLog Error) $ fmap (T.pack . show) errors
128− return Nothing
129−
130−withUTF8FileContentsM :: (MonadIO m, HasCliConfig e m) => FilePath -> (ByteString -> CliT e IO a) -> m a
131−withUTF8FileContentsM fp f = do
132− c <- getCliConfig
133− liftIO $ withUTF8FileContents fp $ runCli c . f . toUTF8BS
220+parseCabalPackage dir = parseCabalPackage' dir >>= \case
221+ Left err -> Nothing <$ putLog Error err
222+ Right (warnings, pkgInfo) -> do
223+ for_ warnings $ putLog Warning . T.pack . show
224+ pure $ Just pkgInfo
225+
226+-- | Like 'parseCabalPackage' but returns errors and warnings directly so as to avoid 'MonadObelisk'.
227+parseCabalPackage'
228+ :: (MonadIO m)
229+ => FilePath -- ^ Package directory
230+ -> m (Either T.Text ([Dist.PWarning], CabalPackageInfo))
231+parseCabalPackage' pkg = runExceptT $ do
232+ (cabalContents, packageFile, packageName) <- guessCabalPackageFile pkg >>= \case
233+ Left GuessPackageFileError_NotFound -> throwError $ "No .cabal or package.yaml file found in " <> T.pack pkg
234+ Left (GuessPackageFileError_Ambiguous _) -> throwError $ "Unable to determine which .cabal file to use in " <> T.pack pkg
235+ Right (Left (CabalFilePath file)) -> (, file, takeBaseName file) <$> liftIO (readUTF8File file)
236+ Right (Right (HPackFilePath file)) -> do
237+ let
238+ decodeOptions = Hpack.DecodeOptions (Hpack.ProgramName "ob") file Nothing Hpack.decodeYaml
239+ liftIO (Hpack.readPackageConfig decodeOptions) >>= \case
240+ Left err -> throwError $ T.pack $ "Failed to parse " <> file <> ": " <> err
241+ Right (Hpack.DecodeResult hpackPackage _ _ _) -> pure (Hpack.renderPackage [] hpackPackage, file, Hpack.packageName hpackPackage)
242+
243+ let
244+ (warnings, result) = runParseResult $ parseGenericPackageDescription $ toUTF8BS cabalContents
245+ osConfVar = case System.Info.os of
246+ "linux" -> Just Dist.Linux
247+ "darwin" -> Just Dist.OSX
248+ _ -> trace "Unrecgonized System.Info.os" Nothing
249+ archConfVar = Just Dist.X86_64 -- TODO: Actually infer this
250+ evalConfVar v = Right $ case v of
251+ OS osVar -> Just osVar == osConfVar
252+ Arch archVar -> Just archVar == archConfVar
253+ Impl GHC _ -> True -- TODO: Actually check version range
254+ _ -> False
255+ case condLibrary <$> result of
256+ Right (Just condLib) -> do
257+ let (_, lib) = simplifyCondTree evalConfVar condLib
258+ pure $ (warnings,) $ CabalPackageInfo
259+ { _cabalPackageInfo_packageName = T.pack packageName
260+ , _cabalPackageInfo_packageFile = packageFile
261+ , _cabalPackageInfo_packageRoot = takeDirectory packageFile
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)
134304
135305-- | Create ghci configuration to load the given packages
136306withGhciScript
137307 :: MonadObelisk m
138− => [FilePath] -- ^ List of packages to load into ghci
139− -> (FilePath -> m ()) -- ^ Action to run with the path to generated temporory .ghci
308+ => [CabalPackageInfo] -- ^ List of packages to load into 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
140311 -> m ()
141−withGhciScript pkgs f = do
142− (pkgDirErrs, packageInfos) <- fmap partitionEithers $ forM pkgs $ \pkg -> do
143− flip fmap (parseCabalPackage pkg) $ \case
144− Nothing -> Left pkg
145− Just packageInfo -> Right packageInfo
146−
147− when (null packageInfos) $
148− failWith $ T.pack $ "No valid pkgs found in " <> intercalate ", " pkgs
149− unless (null pkgDirErrs) $
150− putLog Warning $ T.pack $ "Failed to find pkgs in " <> intercalate ", " pkgDirErrs
151−
152− let extensions = packageInfos >>= _cabalPackageInfo_defaultExtensions
153− extensionsLine = if extensions == mempty
154− then ""
155− else ":set " <> intercalate " " ((("-X" <>) . prettyShow) <$> extensions)
156− dotGhci = unlines $
157− [ ":set -i" <> intercalate ":" (packageInfos >>= rootedSourceDirs)
158− , extensionsLine
159− , ":load Backend Frontend"
160− , "import Obelisk.Run"
161− , "import qualified Frontend"
162− , "import qualified Backend"
163− ]
312+withGhciScript packageInfos pathBase f = do
313+ selfExe <- liftIO getExecutablePath
314+ let
315+ packageNames = Set.fromList $ map _cabalPackageInfo_packageName packageInfos
316+ modulesToLoad = mconcat
317+ [ [ "Obelisk.Run" | "obelisk-run" `Set.member` packageNames ]
318+ , [ "Backend" | "backend" `Set.member` packageNames ]
319+ , [ "Frontend" | "frontend" `Set.member` packageNames ]
320+ ]
321+ dotGhci = unlines
322+ -- TODO: Shell escape
323+ [ ":set -F -pgmF " <> selfExe <> " -optF " <> preprocessorIdentifier <> " " <> unwords (map (("-optF " <>) . makeRelative pathBase . _cabalPackageInfo_packageFile) packageInfos)
324+ , ":set -i" <> intercalate ":" (packageInfos >>= rootedSourceDirs)
325+ , if null modulesToLoad then "" else ":load " <> unwords modulesToLoad
326+ , "import qualified Obelisk.Run"
327+ , "import qualified Frontend"
328+ , "import qualified Backend" ]
164329 withSystemTempDirectory "ob-ghci" $ \fp -> do
165330 let dotGhciPath = fp </> ".ghci"
166331 liftIO $ writeFile dotGhciPath dotGhci
@@ -168,31 +333,52 @@ withGhciScript pkgs f = do
168333
169334 where
170335 rootedSourceDirs pkg = NE.toList $
171− (_cabalPackageInfo_packageRoot pkg </>) <$> _cabalPackageInfo_sourceDirs pkg
336+ makeRelative pathBase . (_cabalPackageInfo_packageRoot pkg </>) <$> _cabalPackageInfo_sourceDirs pkg
172337
173338-- | Run ghci repl
174339runGhciRepl
175340 :: MonadObelisk m
176− => FilePath -- ^ Path to .ghci
341+ => FilePath -- ^ Path to project root
342+ -> [CabalPackageInfo]
343+ -> FilePath -- ^ Path to .ghci
177344 -> 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
179349
180350-- | Run ghcid
181351runGhcid
182352 :: 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]
184357 -> Maybe String -- ^ Optional command to run at every reload
185358 -> 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
187361 where
188362 opts =
189363 [ "-W"
190− --TODO: The decision of whether to use -fwarn-redundant-constraints should probably be made by the user
191− , "--command='ghci -Wall -ignore-dot-ghci -fwarn-redundant-constraints -no-user-package-db -ghci-script " <> dotGhci <> "' "
192− , "--reload=config"
364+ , "--command='ghci -ignore-dot-ghci " <> makeBaseGhciOptions dotGhci <> "' "
193365 , "--outputfile=ghcid-output.txt"
194− ] <> testCmd
195− testCmd = maybeToList (flip fmap mcmd $ \cmd -> "--test='" <> cmd <> "'")
366+ ] <> map (\x -> "--reload='" <> x <> "'") reloadFiles
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+ ]
196382
197383getFreePort :: MonadIO m => m PortNumber
198384getFreePort = liftIO $ withSocketsDo $ do
lib/command/src/Obelisk/Command/Thunk.hs+259−127
@@ -8,26 +8,31 @@
88{-# LANGUAGE ScopedTypeVariables #-}
99{-# LANGUAGE TypeApplications #-}
1010module Obelisk.Command.Thunk
11− ( ThunkPtr (..)
11+ ( GitHubSource (..)
12+ , GitUri (..)
13+ , ReadThunkError (..)
14+ , ThunkConfig (..)
15+ , ThunkData (..)
16+ , ThunkPackConfig (..)
17+ , ThunkPtr (..)
1218 , ThunkRev (..)
1319 , ThunkSource (..)
14− , ThunkData (..)
15− , ReadThunkError (..)
16− , GitHubSource (..)
17− , getThunkGitBranch
18− , getLatestRev
19− , updateThunkToLatest
20+ , ThunkUpdateConfig (..)
21+ , attrCacheFileName
2022 , createThunk
2123 , createThunkWithLatest
24+ , getLatestRev
25+ , getThunkGitBranch
26+ , getThunkPtr
27+ , gitUriToText
2228 , nixBuildAttrWithCache
2329 , nixBuildThunkAttrWithCache
24− , unpackThunk
2530 , packThunk
31+ , parseGitUri
2632 , readThunk
33+ , unpackThunk
2734 , updateThunk
28− , getThunkPtr
29− , getThunkPtr'
30− , parseGitUri
35+ , updateThunkToLatest
3136 , uriThunkPtr
3237 ) where
3338
@@ -43,6 +48,7 @@ import qualified Data.Aeson as Aeson
4348import Data.Aeson.Encode.Pretty
4449import qualified Data.Aeson.Types as Aeson
4550import qualified Data.ByteString.Lazy as LBS
51+import Data.Containers.ListUtils (nubOrd)
4652import Data.Default
4753import Data.Either.Combinators (fromRight', rightToMaybe)
4854import Data.Git.Ref (Ref)
@@ -65,12 +71,12 @@ import GitHub
6571import GitHub.Data.Name
6672import Obelisk.Command.Nix
6773import System.Directory
74+import System.Exit
6875import System.FilePath
6976import System.IO.Error
7077import System.IO.Temp
7178import System.Posix (getSymbolicLinkStatus, modificationTime)
72−import System.Process (proc)
73−import Text.URI
79+import qualified Text.URI as URI
7480
7581import Obelisk.App (MonadObelisk)
7682import Obelisk.CliApp
@@ -95,7 +101,7 @@ type NixSha256 = Text --TODO: Use a smart constructor and make this actually ver
95101
96102-- | A specific revision of data; it may be available from multiple sources
97103data ThunkRev = ThunkRev
98− { _thunkRev_commit :: Ref
104+ { _thunkRev_commit :: Ref Ref.SHA1
99105 , _thunkRev_nixSha256 :: NixSha256
100106 }
101107 deriving (Show, Eq, Ord)
@@ -112,37 +118,63 @@ data GitHubSource = GitHubSource
112118 { _gitHubSource_owner :: Name Owner
113119 , _gitHubSource_repo :: Name Repo
114120 , _gitHubSource_branch :: Maybe (Name Branch)
121+ , _gitHubSource_private :: Bool
115122 }
116123 deriving (Show, Eq, Ord)
117124
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+
118134data GitSource = GitSource
119− { _gitSource_url :: URI
135+ { _gitSource_url :: GitUri
120136 , _gitSource_branch :: Maybe (Name Branch)
121137 , _gitSource_fetchSubmodules :: Bool
138+ , _gitSource_private :: Bool
122139 }
123140 deriving (Show, Eq, Ord)
124141
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+
125156-- | Convert a GitHub source to a regular Git source. Assumes no submodules.
126157forgetGithub :: Bool -> GitHubSource -> GitSource
127158forgetGithub useSsh s = GitSource
128− { _gitSource_url = URI
129− { uriScheme = Just $ fromRight' $ mkScheme $ if useSsh then "ssh" else "https"
130− , uriAuthority = Right $ Authority
131− { authUserInfo = UserInfo (fromRight' $ mkUsername "git") Nothing
159+ { _gitSource_url = GitUri $ URI.URI
160+ { URI.uriScheme = Just $ fromRight' $ URI.mkScheme $ if useSsh then "ssh" else "https"
161+ , URI.uriAuthority = Right $ URI.Authority
162+ { URI.authUserInfo = URI.UserInfo (fromRight' $ URI.mkUsername "git") Nothing
132163 <$ guard useSsh
133− , authHost = fromRight' $ mkHost "github.com"
134− , authPort = Nothing
164+ , URI.authHost = fromRight' $ URI.mkHost "github.com"
165+ , URI.authPort = Nothing
135166 }
136− , uriPath = Just ( False
137− , fromRight' . mkPathPiece <$>
167+ , URI.uriPath = Just ( False
168+ , fromRight' . URI.mkPathPiece <$>
138169 untagName (_gitHubSource_owner s)
139170 :| [ untagName (_gitHubSource_repo s) <> ".git" ]
140171 )
141− , uriQuery = []
142− , uriFragment = Nothing
172+ , URI.uriQuery = []
173+ , URI.uriFragment = Nothing
143174 }
144175 , _gitSource_branch = _gitHubSource_branch s
145176 , _gitSource_fetchSubmodules = False
177+ , _gitSource_private = _gitHubSource_private s
146178 }
147179
148180getThunkGitBranch :: ThunkPtr -> Maybe Text
@@ -150,26 +182,26 @@ getThunkGitBranch (ThunkPtr _ src) = fmap untagName $ case src of
150182 ThunkSource_GitHub s -> _gitHubSource_branch s
151183 ThunkSource_Git s -> _gitSource_branch s
152184
153−commitNameToRef :: Name Commit -> Ref
185+commitNameToRef :: Name Commit -> Ref Ref.SHA1
154186commitNameToRef (N c) = Ref.fromHex $ encodeUtf8 c
155187
156188-- TODO: Use spinner here.
157189getNixSha256ForUriUnpacked
158190 :: MonadObelisk m
159− => URI
191+ => GitUri
160192 -> m NixSha256
161193getNixSha256ForUriUnpacked 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
163195 [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]
165197 pure hash
166198
167−nixPrefetchGit :: MonadObelisk m => URI -> Text -> Bool -> m NixSha256
199+nixPrefetchGit :: MonadObelisk m => GitUri -> Text -> Bool -> m NixSha256
168200nixPrefetchGit 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
170202 out <- readProcessAndLogStderr Debug $
171203 proc "nix-prefetch-git" $ filter (/="")
172− [ "--url", T.unpack $ render uri
204+ [ "--url", T.unpack $ gitUriToText uri
173205 , "--rev", T.unpack rev
174206 , if fetchSubmodules then "--fetch-submodules" else ""
175207 , "--quiet"
@@ -200,6 +232,9 @@ readThunk thunkDir = do
200232 False -> return $ return Nothing
201233 False -> liftIO $ fmap ThunkData_Packed <$> readPackedThunk thunkDir
202234
235+attrCacheFileName :: FilePath
236+attrCacheFileName = ".attr-cache"
237+
203238data ThunkType = ThunkType
204239 { _thunkType_loader :: FilePath
205240 , _thunkType_json :: FilePath
@@ -212,7 +247,7 @@ gitHubThunkType :: ThunkType
212247gitHubThunkType = ThunkType
213248 { _thunkType_loader = "default.nix"
214249 , _thunkType_json = "github.json"
215− , _thunkType_optional = Set.fromList [".attr-cache"]
250+ , _thunkType_optional = Set.fromList [attrCacheFileName]
216251 , _thunkType_loaderVersions = gitHubStandaloneLoaders
217252 , _thunkType_parser = parseThunkPtr $ \v ->
218253 ThunkSource_GitHub <$> parseGitHubSource v <|> ThunkSource_Git <$> parseGitSource v
@@ -222,7 +257,7 @@ gitThunkType :: ThunkType
222257gitThunkType = ThunkType
223258 { _thunkType_loader = "default.nix"
224259 , _thunkType_json = "git.json"
225− , _thunkType_optional = Set.fromList [".attr-cache"]
260+ , _thunkType_optional = Set.fromList [attrCacheFileName]
226261 , _thunkType_loaderVersions = plainGitStandaloneLoaders
227262 , _thunkType_parser = parseThunkPtr $ fmap ThunkSource_Git . parseGitSource
228263 }
@@ -287,10 +322,12 @@ parseGitHubSource v = do
287322 owner <- v Aeson..: "owner"
288323 repo <- v Aeson..: "repo"
289324 branch <- v Aeson..:! "branch"
325+ private <- v Aeson..:? "private"
290326 pure $ GitHubSource
291327 { _gitHubSource_owner = owner
292328 , _gitHubSource_repo = repo
293329 , _gitHubSource_branch = branch
330+ , _gitHubSource_private = fromMaybe False private
294331 }
295332
296333parseGitSource :: Aeson.Object -> Aeson.Parser GitSource
@@ -298,10 +335,12 @@ parseGitSource v = do
298335 Just url <- parseGitUri <$> v Aeson..: "url"
299336 branch <- v Aeson..:! "branch"
300337 fetchSubmodules <- v Aeson..:! "fetchSubmodules"
338+ private <- v Aeson..:? "private"
301339 pure $ GitSource
302340 { _gitSource_url = url
303341 , _gitSource_branch = branch
304342 , _gitSource_fetchSubmodules = fromMaybe False fetchSubmodules
343+ , _gitSource_private = fromMaybe False private
305344 }
306345
307346overwriteThunk :: MonadObelisk m => FilePath -> ThunkPtr -> m ()
@@ -328,13 +367,15 @@ encodeThunkPtrData (ThunkPtr rev src) = case src of
328367 , ("branch" .=) <$> _gitHubSource_branch s
329368 , Just $ "rev" .= Ref.toHexString (_thunkRev_commit rev)
330369 , Just $ "sha256" .= _thunkRev_nixSha256 rev
370+ , Just $ "private" .= _gitHubSource_private s
331371 ]
332372 ThunkSource_Git s -> encodePretty' plainGitCfg $ Aeson.object $ catMaybes
333− [ Just $ "url" .= render (_gitSource_url s)
373+ [ Just $ "url" .= gitUriToText (_gitSource_url s)
334374 , Just $ "rev" .= Ref.toHexString (_thunkRev_commit rev)
335375 , ("branch" .=) <$> _gitSource_branch s
336376 , Just $ "sha256" .= _thunkRev_nixSha256 rev
337377 , Just $ "fetchSubmodules" .= _gitSource_fetchSubmodules s
378+ , Just $ "private" .= _gitSource_private s
338379 ]
339380 where
340381 githubCfg = defConfig
@@ -355,6 +396,7 @@ encodeThunkPtrData (ThunkPtr rev src) = case src of
355396 [ "url"
356397 , "rev"
357398 , "sha256"
399+ , "private"
358400 , "fetchSubmodules"
359401 ] <> compare
360402 , confTrailingNewline = True
@@ -362,7 +404,7 @@ encodeThunkPtrData (ThunkPtr rev src) = case src of
362404
363405createThunk :: MonadIO m => FilePath -> ThunkPtr -> m ()
364406createThunk target thunk = liftIO $ do
365− createDirectoryIfMissing True (target </> ".attr-cache")
407+ createDirectoryIfMissing True (target </> attrCacheFileName)
366408 T.writeFile (target </> "default.nix") (thunkPtrLoader thunk)
367409 let
368410 jsonFileName = case _thunkPtr_source thunk of
@@ -378,8 +420,8 @@ createThunkWithLatest target s = do
378420 , _thunkPtr_rev = rev
379421 }
380422
381−updateThunkToLatest :: MonadObelisk m => FilePath -> Maybe String -> m ()
382−updateThunkToLatest target mBranch = withSpinner' ("Updating thunk " <> T.pack target <> " to latest") (pure $ const $ "Thunk " <> T.pack target <> " updated to latest") $ do
423+updateThunkToLatest :: MonadObelisk m => ThunkUpdateConfig -> FilePath -> m ()
424+updateThunkToLatest (ThunkUpdateConfig mBranch thunkConfig) target = withSpinner' ("Updating thunk " <> T.pack target <> " to latest") (pure $ const $ "Thunk " <> T.pack target <> " updated to latest") $ do
383425 checkThunkDirectory "ob thunk update directory cannot be '.'" target
384426 -- check to see if thunk should be updated to a specific branch or just update it's current branch
385427 case mBranch of
@@ -391,7 +433,7 @@ updateThunkToLatest target mBranch = withSpinner' ("Updating thunk " <> T.pack t
391433 ThunkData_Checkout _ -> failWith "cannot update an unpacked thunk"
392434 let src = _thunkPtr_source ptr
393435 rev <- getLatestRev src
394− overwriteThunk overwrite $ ThunkPtr
436+ overwriteThunk overwrite $ modifyThunkPtrByConfig thunkConfig $ ThunkPtr
395437 { _thunkPtr_source = src
396438 , _thunkPtr_rev = rev
397439 }
@@ -399,24 +441,25 @@ updateThunkToLatest target mBranch = withSpinner' ("Updating thunk " <> T.pack t
399441 Left err -> failWith $ T.pack $ "thunk update: " <> show err
400442 Right c -> case c of
401443 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
403445 ThunkSource_GitHub tsgh -> do
404446 let tsg = forgetGithub False tsgh
405− setThunk 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."
447+ setThunk thunkConfig target tsg branch
448+ ThunkData_Checkout _ -> failWith $ T.pack $ "thunk located at " <> show target <> " is unpacked. Use ob thunk pack on the desired directory and then try ob thunk update again."
407449
408−setThunk :: MonadObelisk m => FilePath -> GitSource -> String -> m ()
409−setThunk target gs branch = do
410− newThunkPtr <- uriThunkPtr (_gitSource_url gs) (Just $ T.pack branch) Nothing
450+setThunk :: MonadObelisk m => ThunkConfig -> FilePath -> GitSource -> String -> m ()
451+setThunk thunkConfig target gs branch = do
452+ newThunkPtr <- uriThunkPtr (_gitSource_url gs) (_thunkConfig_private thunkConfig) (Just $ T.pack branch) Nothing
411453 overwriteThunk target newThunkPtr
412− updateThunkToLatest target Nothing
454+ updateThunkToLatest (ThunkUpdateConfig Nothing thunkConfig) target
413455
414456-- | All recognized github standalone loaders, ordered from newest to oldest.
415457-- This tool will only ever produce the newest one when it writes a thunk.
416458gitHubStandaloneLoaders :: NonEmpty Text
417459gitHubStandaloneLoaders =
418− gitHubStandaloneLoaderV2 :|
460+ gitHubStandaloneLoaderV4 :|
419461 [ gitHubStandaloneLoaderV3
462+ , gitHubStandaloneLoaderV2
420463 , gitHubStandaloneLoaderV1
421464 ]
422465
@@ -456,10 +499,24 @@ gitHubStandaloneLoaderV3 = T.unlines
456499 , "in import (fetch (builtins.fromJSON (builtins.readFile ./github.json)))"
457500 ]
458501
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+
459514plainGitStandaloneLoaders :: NonEmpty Text
460515plainGitStandaloneLoaders =
461− plainGitStandaloneLoaderV1 :|
462− [ plainGitStandaloneLoaderV2
516+ plainGitStandaloneLoaderV4 :|
517+ [ plainGitStandaloneLoaderV1
518+ , plainGitStandaloneLoaderV2
519+ , plainGitStandaloneLoaderV3
463520 ]
464521
465522plainGitStandaloneLoaderV1 :: Text
@@ -480,6 +537,41 @@ plainGitStandaloneLoaderV2 = T.unlines
480537 , "in import (fetchGit (builtins.fromJSON (builtins.readFile ./git.json)))"
481538 ]
482539
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+
483575thunkFileNames :: [FilePath]
484576thunkFileNames = ["default.nix", "github.json", "git.json"]
485577
@@ -506,7 +598,7 @@ nixBuildThunkAttrWithCache thunkDir attr = do
506598 let cacheErrHandler e
507599 | isDoesNotExistError e = return Nothing -- expected from a cache miss
508600 | otherwise = putLog Error (T.pack $ displayException e) >> return Nothing
509− cacheDir = thunkDir </> ".attr-cache"
601+ cacheDir = thunkDir </> attrCacheFileName
510602 cachePath = cacheDir </> attr <.> "out"
511603 latestChange <- liftIO $ do
512604 createDirectoryIfMissing False cacheDir
@@ -521,7 +613,7 @@ nixBuildThunkAttrWithCache thunkDir attr = do
521613 Just c -> return c
522614 Nothing -> do
523615 putLog Warning $ T.pack $ mconcat [thunkDir, ": ", attr, " not cached, building ..."]
524− _ <- nixCmd $ NixCmd_Build$ def
616+ _ <- nixCmd $ NixCmd_Build $ def
525617 Lens.& nixBuildConfig_outLink Lens..~ OutLink_IndirectRoot cachePath
526618 Lens.& nixCmdConfig_target Lens..~ Target
527619 { _target_path = Just thunkDir
@@ -569,13 +661,13 @@ updateThunk p f = withSystemTempDirectory "obelisk-thunkptr-" $ \tmpDir -> do
569661 Right (ThunkData_Packed _) -> do
570662 let tmpThunk = tmpDir </> "thunk"
571663 callProcessAndLogOutput (Notice, Error) $
572− proc "cp" ["-r", "-T", thunkDir, tmpThunk]
664+ proc cp ["-r", "-T", thunkDir, tmpThunk]
573665 return tmpThunk
574− Right _ -> failWith $ "Thunk is not packed"
666+ Right _ -> failWith "Thunk is not packed"
575667 updateThunkFromTmp p' = do
576− _ <- packThunk' True p'
668+ _ <- packThunk' True (ThunkPackConfig False (ThunkConfig Nothing)) p'
577669 callProcessAndLogOutput (Notice, Error) $
578− proc "cp" ["-r", "-T", p', p]
670+ proc cp ["-r", "-T", p', p]
579671
580672finalMsg :: Bool -> (a -> Text) -> Maybe (a -> Text)
581673finalMsg noTrail s = if noTrail then Nothing else Just s
@@ -607,7 +699,7 @@ unpackThunk' noTrail thunkDir = checkThunkDirectory "Can't pack/unpack from with
607699 let git = callProcessAndLogOutput (Notice, Notice) . gitProc tmpRepo
608700 git $ [ "clone" ]
609701 ++ ("--recursive" <$ guard (_gitSource_fetchSubmodules s))
610− ++ [ T.unpack $ render $ _gitSource_url s ]
702+ ++ [ T.unpack $ gitUriToText $ _gitSource_url s ]
611703 ++ do branch <- maybeToList $ _gitSource_branch s
612704 [ "--branch", T.unpack $ untagName branch ]
613705 git ["reset", "--hard", Ref.toHexString $ _thunkRev_commit $ _thunkPtr_rev tptr]
@@ -616,35 +708,39 @@ unpackThunk' noTrail thunkDir = checkThunkDirectory "Can't pack/unpack from with
616708
617709 liftIO $ createDirectory obGitDir
618710 callProcessAndLogOutput (Notice, Error) $
619− proc "cp" ["-r", "-T", thunkDir </> ".", obGitDir </> "orig-thunk"]
711+ proc cp ["-r", "-T", thunkDir </> ".", obGitDir </> "orig-thunk"]
620712 callProcessAndLogOutput (Notice, Error) $
621713 proc "rm" ["-r", thunkDir]
622714 callProcessAndLogOutput (Notice, Error) $
623715 proc "mv" ["-T", tmpRepo, thunkDir]
624716
625−--TODO: add force mode to pack even if changes are present
626717--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
628719packThunk = packThunk' False
629720
630−packThunk' :: MonadObelisk m => Bool -> FilePath -> m ThunkPtr
631−packThunk' noTrail thunkDir = checkThunkDirectory "Can't pack/unpack from within the thunk directory" thunkDir >> readThunk thunkDir >>= \case
721+packThunk' :: MonadObelisk m => Bool -> ThunkPackConfig -> FilePath -> m ThunkPtr
722+packThunk' noTrail (ThunkPackConfig force thunkConfig) thunkDir = checkThunkDirectory "Can't pack/unpack from within the thunk directory" thunkDir >> readThunk thunkDir >>= \case
632723 Left err -> failWith $ T.pack $ "thunk pack: " <> show err
633724 Right (ThunkData_Packed _) -> failWith "pack: thunk is already packed"
634725 Right (ThunkData_Checkout _) -> do
635726 withSpinner' ("Packing thunk " <> T.pack thunkDir)
636727 (finalMsg noTrail $ const $ "Packed thunk " <> T.pack thunkDir) $ do
637− thunkPtr <- getThunkPtr thunkDir
728+ thunkPtr <- modifyThunkPtrByConfig thunkConfig <$> getThunkPtr (not force) thunkDir (_thunkConfig_private thunkConfig)
638729 callProcessAndLogOutput (Debug, Error) $ proc "rm" ["-rf", thunkDir]
639730 liftIO $ createThunk thunkDir thunkPtr
640731 pure thunkPtr
641732
642−getThunkPtr :: MonadObelisk m => FilePath -> m ThunkPtr
643−getThunkPtr = getThunkPtr' True
733+modifyThunkPtrByConfig :: ThunkConfig -> ThunkPtr -> ThunkPtr
734+modifyThunkPtrByConfig (ThunkConfig markPrivate') ptr = case markPrivate' of
735+ Nothing -> ptr
736+ Just markPrivate -> ptr { _thunkPtr_source = case _thunkPtr_source ptr of
737+ ThunkSource_Git s -> ThunkSource_Git $ s { _gitSource_private = markPrivate }
738+ ThunkSource_GitHub s -> ThunkSource_GitHub $ s { _gitHubSource_private = markPrivate }
739+ }
644740
645−getThunkPtr' :: forall m. MonadObelisk m => Bool -> FilePath -> m ThunkPtr
646−getThunkPtr' checkClean thunkDir = do
647− when checkClean $ ensureCleanGitRepo thunkDir True $
741+getThunkPtr :: forall m. MonadObelisk m => Bool -> FilePath -> Maybe Bool -> m ThunkPtr
742+getThunkPtr checkClean thunkDir mPrivate = do
743+ when checkClean $ ensureCleanGitRepo thunkDir True
648744 "thunk pack: thunk checkout contains unsaved modifications"
649745
650746 -- Check whether there are any stashes
@@ -657,7 +753,7 @@ getThunkPtr' checkClean thunkDir = do
657753 ] ++ T.lines stashOutput
658754 True -> return ()
659755
660− -- Get current branch ``
756+ -- Get current branch
661757 (mCurrentBranch, mCurrentCommit) <- do
662758 b <- listToMaybe
663759 <$> T.lines
@@ -666,7 +762,7 @@ getThunkPtr' checkClean thunkDir = do
666762 <$> T.lines
667763 <$> readGitProcess thunkDir ["rev-parse", "HEAD"]
668764 case b of
669− (Just "HEAD") -> failWith $ T.unlines $
765+ (Just "HEAD") -> failWith $ T.unlines
670766 [ "thunk pack: You are in 'detached HEAD' state."
671767 , "If you want to pack at the current ref \
672768 \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
738834 [ "thunk pack: Certain branches in the thunk have commits not yet pushed upstream:"
739835 , ""
740836 ] ++
741− (flip map (Map.toList nonGood) $ \(branch, (upstream, (ahead, behind))) -> mconcat
837+ flip map (Map.toList nonGood) (\(branch, (upstream, (ahead, behind))) -> mconcat
742838 [" ", branch, " ahead: ", T.pack (show ahead), " behind: ", T.pack (show behind), " remote branch ", upstream]) ++
743839 [ ""
744840 , "Please push these upstream and try again. (Or just fetch, if they are somehow \
@@ -746,7 +842,7 @@ getThunkPtr' checkClean thunkDir = do
746842 ]
747843
748844 -- 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."
750846
751847 let remote = maybe "origin" snd $ flip Map.lookup headUpstream =<< mCurrentBranch
752848
@@ -759,7 +855,7 @@ getThunkPtr' checkClean thunkDir = do
759855 remoteUri <- case parseGitUri remoteUri' of
760856 Nothing -> failWith $ "Could not identify git remote: " <> remoteUri'
761857 Just uri -> pure uri
762− uriThunkPtr remoteUri mCurrentBranch mCurrentCommit
858+ uriThunkPtr remoteUri mPrivate mCurrentBranch mCurrentCommit
763859
764860-- | Get the latest revision available from the given source
765861getLatestRev :: MonadObelisk m => ThunkSource -> m ThunkRev
@@ -778,18 +874,18 @@ getLatestRev os = do
778874-- performance. If that doesn't work (e.g. authentication issue), we fall back
779875-- on just doing things the normal way for git repos in general, and save it as
780876-- a regular git thunk.
781−uriThunkPtr :: MonadObelisk m => URI -> Maybe Text -> Maybe Text -> m ThunkPtr
782−uriThunkPtr uri mbranch mcommit = do
877+uriThunkPtr :: MonadObelisk m => GitUri -> Maybe Bool -> Maybe Text -> Maybe Text -> m ThunkPtr
878+uriThunkPtr uri mPrivate mbranch mcommit = do
783879 commit <- case mcommit of
784880 Nothing -> gitGetCommitBranch uri mbranch >>= return . snd
785881 (Just c) -> return c
786− (src, rev) <- case uriToThunkSource uri mbranch of
882+ (src, rev) <- uriToThunkSource uri mPrivate mbranch >>= \case
787883 ThunkSource_GitHub s -> do
788884 rev <- runExceptT $ githubThunkRev s commit
789885 case rev of
790886 Right r -> pure (ThunkSource_GitHub s, r)
791887 Left e -> do
792− putLog Warning $ "\
888+ putLog Warning "\
793889\Failed to fetch archive from GitHub. This is probably a private repo. \
794890\Falling back on normal fetchgit. Original failure:"
795891 errorToWarning e
@@ -806,34 +902,66 @@ uriThunkPtr uri mbranch mcommit = do
806902-- If the thunk is a GitHub thunk and fails, we do *not* fall back like with
807903-- `uriThunkPtr`. Unlike a plain URL, a thunk src explicitly states which method
808904-- should be employed, and so we respect that.
809−uriToThunkSource :: URI -> Maybe Text -> ThunkSource
810−uriToThunkSource u
811− | Right uriAuth <- uriAuthority u
812− , Just scheme <- unRText <$> uriScheme u
905+uriToThunkSource :: MonadObelisk m => GitUri -> Maybe Bool -> Maybe Text -> m ThunkSource
906+uriToThunkSource (GitUri u) mPrivate
907+ | Right uriAuth <- URI.uriAuthority u
908+ , Just scheme <- URI.unRText <$> URI.uriScheme u
813909 , case scheme of
814− "ssh" -> uriAuth == Authority
815− { authUserInfo = Just $ UserInfo (fromRight' $ mkUsername "git") Nothing
816− , authHost = fromRight' $ mkHost "github.com"
817− , authPort = Nothing
910+ "ssh" -> uriAuth == URI.Authority
911+ { URI.authUserInfo = Just $ URI.UserInfo (fromRight' $ URI.mkUsername "git") Nothing
912+ , URI.authHost = fromRight' $ URI.mkHost "github.com"
913+ , URI.authPort = Nothing
818914 }
819915 s -> s `L.elem` [ "git", "https", "http" ] -- "http:" just redirects to "https:"
820− && unRText (authHost uriAuth) == "github.com"
821− , Just (_, owner :| [repoish]) <- uriPath u
822− = \mbranch -> ThunkSource_GitHub $ GitHubSource
823− { _gitHubSource_owner = N $ unRText owner
824− , _gitHubSource_repo = N $ let
825− repoish' = unRText repoish
826− in case T.stripSuffix ".git" repoish' of
827− Just repo -> repo
828− Nothing -> repoish'
829− , _gitHubSource_branch = N <$> mbranch
830− }
916+ && URI.unRText (URI.authHost uriAuth) == "github.com"
917+ , Just (_, owner :| [repoish]) <- URI.uriPath u
918+ = \mbranch -> do
919+ isPrivate <- getIsPrivate
920+ pure $ ThunkSource_GitHub $ GitHubSource
921+ { _gitHubSource_owner = N $ URI.unRText owner
922+ , _gitHubSource_repo = N $ let
923+ repoish' = URI.unRText repoish
924+ in fromMaybe repoish' $ T.stripSuffix ".git" repoish'
925+ , _gitHubSource_branch = N <$> mbranch
926+ , _gitHubSource_private = isPrivate
927+ }
831928
832− | otherwise = \mbranch -> ThunkSource_Git $ GitSource
833− { _gitSource_url = u
834− , _gitSource_branch = N <$> mbranch
835− , _gitSource_fetchSubmodules = False -- TODO: How do we determine if this should be true?
836− }
929+ | otherwise = \mbranch -> do
930+ isPrivate <- getIsPrivate
931+ pure $ ThunkSource_Git $ GitSource
932+ { _gitSource_url = GitUri u
933+ , _gitSource_branch = N <$> mbranch
934+ , _gitSource_fetchSubmodules = False -- TODO: How do we determine if this should be true?
935+ , _gitSource_private = isPrivate
936+ }
937+ where
938+ getIsPrivate = maybe (guessGitRepoIsPrivate $ GitUri u) pure mPrivate
939+
940+guessGitRepoIsPrivate :: MonadObelisk m => GitUri -> m Bool
941+guessGitRepoIsPrivate uri = flip fix urisToTry $ \loop -> \case
942+ [] -> pure True
943+ uriAttempt:xs -> do
944+ result <- readCreateProcessWithExitCode $
945+ isolateGitProc $
946+ gitProcNoRepo
947+ [ "ls-remote"
948+ , "--quiet"
949+ , "--exit-code"
950+ , "--symref"
951+ , T.unpack $ gitUriToText uriAttempt
952+ ]
953+ case result of
954+ (ExitSuccess, _, _) -> pure False -- Must be a public repo
955+ _ -> loop xs
956+ where
957+ urisToTry = nubOrd $
958+ -- Include the original URI if it isn't using SSH because SSH will certainly fail.
959+ [uri | fmap URI.unRText (URI.uriScheme (unGitUri uri)) /= Just "ssh"] <>
960+ [changeScheme "https" uri, changeScheme "http" uri]
961+ changeScheme scheme (GitUri u) = GitUri $ u
962+ { URI.uriScheme = URI.mkScheme scheme
963+ , URI.uriAuthority = (\x -> x { URI.authUserInfo = Nothing }) <$> URI.uriAuthority u
964+ }
837965
838966-- Funny signature indicates no effects depend on the optional branch name.
839967githubThunkRev
@@ -845,19 +973,19 @@ githubThunkRev
845973githubThunkRev s commit = do
846974 owner <- forcePP $ _gitHubSource_owner s
847975 repo <- forcePP $ _gitHubSource_repo s
848− revTarball <- mkPathPiece $ commit <> ".tar.gz"
849− let archiveUri = URI
850− { uriScheme = Just $ fromRight' $ mkScheme "https"
851− , uriAuthority = Right $ Authority
852− { authUserInfo = Nothing
853− , authHost = fromRight' $ mkHost "github.com"
854− , authPort = Nothing
976+ revTarball <- URI.mkPathPiece $ commit <> ".tar.gz"
977+ let archiveUri = GitUri $ URI.URI
978+ { URI.uriScheme = Just $ fromRight' $ URI.mkScheme "https"
979+ , URI.uriAuthority = Right $ URI.Authority
980+ { URI.authUserInfo = Nothing
981+ , URI.authHost = fromRight' $ URI.mkHost "github.com"
982+ , URI.authPort = Nothing
855983 }
856− , uriPath = Just ( False
857− , owner :| [ repo, fromRight' $ mkPathPiece "archive", revTarball ]
984+ , URI.uriPath = Just ( False
985+ , owner :| [ repo, fromRight' $ URI.mkPathPiece "archive", revTarball ]
858986 )
859− , uriQuery = []
860− , uriFragment = Nothing
987+ , URI.uriQuery = []
988+ , URI.uriFragment = Nothing
861989 }
862990 hash <- getNixSha256ForUriUnpacked archiveUri
863991 putLog Debug $ "Nix sha256 is " <> hash
@@ -866,8 +994,8 @@ githubThunkRev s commit = do
866994 , _thunkRev_nixSha256 = hash
867995 }
868996 where
869− forcePP :: Name entity -> m (RText 'PathPiece)
870− forcePP = mkPathPiece . untagName
997+ forcePP :: Name entity -> m (URI.RText 'URI.PathPiece)
998+ forcePP = URI.mkPathPiece . untagName
871999
8721000gitThunkRev
8731001 :: MonadObelisk m
@@ -876,11 +1004,11 @@ gitThunkRev
8761004 -> m ThunkRev
8771005gitThunkRev s commit = do
8781006 let u = _gitSource_url s
879− protocols = ["https", "ssh", "git"]
880− Just scheme <- pure $ unRText <$> uriScheme u
1007+ protocols = ["file", "https", "ssh", "git"]
1008+ scheme = maybe "file" URI.unRText $ URI.uriScheme $ (\(GitUri x) -> x) u
8811009 unless (T.toLower scheme `elem` protocols) $
8821010 failWith $ "obelisk currently only supports "
883− <> T.intercalate ", " protocols <> " protocols for non-GitHub remotes"
1011+ <> T.intercalate ", " protocols <> " protocols for plain Git remotes"
8841012 hash <- nixPrefetchGit u commit $ _gitSource_fetchSubmodules s
8851013 putLog Informational $ "Nix sha256 is " <> hash
8861014 pure $ ThunkRev
@@ -893,11 +1021,12 @@ gitThunkRev s commit = do
8931021--
8941022-- If the branch name is passed in, it is returned exactly as-is. If it is not
8951023-- passed it, the default branch of the repo is used instead.
1024+
8961025gitGetCommitBranch
897− :: MonadObelisk m => URI -> Maybe Text -> m (Text, CommitId)
1026+ :: MonadObelisk m => GitUri -> Maybe Text -> m (Text, CommitId)
8981027gitGetCommitBranch uri mbranch = withExitFailMessage ("Failure for git remote " <> uriMsg) $ do
8991028 (_, bothMaps) <- gitLsRemote
900− (T.unpack $ render uri)
1029+ (T.unpack $ gitUriToText uri)
9011030 (GitRef_Branch <$> mbranch)
9021031 Nothing
9031032 branch <- case mbranch of
@@ -913,18 +1042,21 @@ gitGetCommitBranch uri mbranch = withExitFailMessage ("Failure for git remote "
9131042 pure (branch, commit)
9141043 where
9151044 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)
9171049
918−parseGitUri :: Text -> Maybe URI
919−parseGitUri x = parseAbsoluteURI x <|> parseSshShorthand x
1050+parseFileURI :: Text -> Maybe URI.URI
1051+parseFileURI uri = if "/" `T.isPrefixOf` uri then parseAbsoluteURI ("file://" <> uri) else Nothing
9201052
921−parseAbsoluteURI :: Text -> Maybe URI
1053+parseAbsoluteURI :: Text -> Maybe URI.URI
9221054parseAbsoluteURI uri = do
923− parsedUri <- mkURI uri
924− guard $ isPathAbsolute parsedUri
925− pure $ parsedUri
1055+ parsedUri <- URI.mkURI uri
1056+ guard $ URI.isPathAbsolute parsedUri
1057+ pure parsedUri
9261058
927−parseSshShorthand :: Text -> Maybe URI
1059+parseSshShorthand :: Text -> Maybe URI.URI
9281060parseSshShorthand uri = do
9291061 -- This is what git does to check that the remote
9301062 -- is not a local file path when parsing shorthand.
@@ -937,4 +1069,4 @@ parseSshShorthand uri = do
9371069 -- This check is used to disambiguate a filepath containing a colon from shorthand
9381070 guard $ isNothing (T.findIndex (=='/') authAndHostname)
9391071 && 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 @@
44{-# LANGUAGE ScopedTypeVariables #-}
55{-# LANGUAGE OverloadedStrings #-}
66{-# LANGUAGE PatternGuards #-}
7+{-# LANGUAGE TemplateHaskell #-}
78module Obelisk.Command.Utils where
89
910import Control.Applicative hiding (many)
@@ -20,18 +21,31 @@ import Data.Maybe (maybeToList)
2021import Data.Text (Text)
2122import qualified Data.Text as T
2223import Data.Void (Void)
23−import System.Directory (canonicalizePath)
24−import System.Environment (getExecutablePath)
2524import System.Exit (ExitCode)
26−import qualified System.Process as P
25+import System.Which (staticWhich)
2726import Text.Megaparsec as MP
2827import Text.Megaparsec.Char as MP
2928
3029import Obelisk.App (MonadObelisk)
3130import Obelisk.CliApp
3231
33−getObeliskExe :: IO FilePath
34−getObeliskExe = getExecutablePath >>= canonicalizePath
32+cp :: FilePath
33+cp = $(staticWhich "cp")
34+
35+ghcidExePath :: FilePath
36+ghcidExePath = $(staticWhich "ghcid")
37+
38+findExePath :: FilePath
39+findExePath = $(staticWhich "find")
40+
41+nixExePath :: FilePath
42+nixExePath = $(staticWhich "nix")
43+
44+nixBuildExePath :: FilePath
45+nixBuildExePath = $(staticWhich "nix-build")
46+
47+jreKeyToolPath :: FilePath
48+jreKeyToolPath = $(staticWhich "keytool")
3549
3650-- Check whether the working directory is clean
3751checkGitCleanStatus :: MonadObelisk m => FilePath -> Bool -> m Bool
@@ -61,20 +75,31 @@ initGit repo = do
6175 git ["add", "."]
6276 git ["commit", "-m", "Initial commit."]
6377
64−gitProcNoRepo :: [String] -> P.CreateProcess
65−gitProcNoRepo = P.proc "git"
78+gitProcNoRepo :: [String] -> ProcessSpec
79+gitProcNoRepo args = setEnvOverride (M.singleton "GIT_TERMINAL_PROMPT" "0" <>) $ proc "git" args
6680
67−gitProc :: FilePath -> [String] -> P.CreateProcess
81+gitProc :: FilePath -> [String] -> ProcessSpec
6882gitProc repo = gitProcNoRepo . runGitInDir
6983 where
7084 runGitInDir args' = case filter (not . null) args' of
7185 args@("clone":_) -> args <> [repo]
7286 args -> ["-C", repo] <> args
7387
74−-- | Recursively copy a directory using `cp -a`
75−copyDir :: FilePath -> FilePath -> P.CreateProcess
88+isolateGitProc :: ProcessSpec -> ProcessSpec
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
76101copyDir 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
78103
79104readGitProcess :: MonadObelisk m => FilePath -> [String] -> m Text
80105readGitProcess repo = readProcessAndLogOutput (Debug, Notice) . gitProc repo
@@ -87,15 +112,15 @@ processToShellString cmd args = unwords $ map quoteAndEscape (cmd : args)
87112 where quoteAndEscape x = T.unpack $ "'" <> T.replace "'" "'\''" (T.pack x) <> "'"
88113
89114-- | 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 ()
91116runProc = callProcessAndLogOutput (Notice, Error)
92117
93118-- | Like runProc, but all output goes to Debug logging level
94−runProcSilently :: MonadObelisk m => P.CreateProcess -> m ()
119+runProcSilently :: MonadObelisk m => ProcessSpec -> m ()
95120runProcSilently = callProcessAndLogOutput (Debug, Debug)
96121
97122-- | 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
99124readProc = readProcessAndLogOutput (Debug, Error)
100125
101126tshow :: Show a => a -> Text
@@ -129,13 +154,13 @@ gitLsRemote repository mRef mBranch = do
129154 (exitCode, out, _err) <- case mBranch of
130155 Nothing -> readCreateProcessWithExitCode $ gitProcNoRepo $
131156 ["ls-remote", "--exit-code", "--symref", repository]
132− ++ (maybeToList $ T.unpack . showGitRef <$> mRef)
133− Just branchName -> readCreateProcessWithExitCode $ gitProcNoRepo $
157+ ++ maybeToList (T.unpack . showGitRef <$> mRef)
158+ Just branchName -> readCreateProcessWithExitCode $ gitProcNoRepo
134159 ["ls-remote", "--exit-code", repository, branchName]
135160 let t = T.pack out
136161 maps <- case MP.runParser parseLsRemote "" t of
137− Left err -> failWith $ T.pack $ MP.parseErrorPretty' t err
138− Right table -> pure $ bimap M.fromList M.fromList $ partitionEithers $ table
162+ Left err -> failWith $ T.pack $ MP.errorBundlePretty err
163+ Right table -> pure $ bimap M.fromList M.fromList $ partitionEithers table
139164 putLog Debug $ "git ls-remote maps: " <> T.pack (show maps)
140165 pure (exitCode, maps)
141166
lib/command/src/Obelisk/Command/VmBuilder.hs+2−2
@@ -18,7 +18,6 @@ import System.Directory (createDirectoryIfMissing)
1818import System.Exit (ExitCode(..))
1919import System.FilePath ((<.>), (</>))
2020import qualified System.Info
21−import System.Process (proc)
2221
2322import Obelisk.App (MonadObelisk, getObeliskUserStateDir)
2423import Obelisk.CliApp
@@ -156,7 +155,8 @@ testLinuxBuild stateDir
156155 | System.Info.os == "linux" = failWith "Using the docker builder is not necessary on linux."
157156 | otherwise = do
158157 (exitCode, _stdout, stderr) <- readCreateProcessWithExitCode $ proc "nix-build"
159− [ "-E", "(import <nixpkgs> { system = \"x86_64-linux\"; }).writeText \"test\" builtins.currentTime"
158+ [ "--no-out-link"
159+ , "-E", "(import <nixpkgs> { system = \"x86_64-linux\"; }).writeText \"test\" builtins.currentTime"
160160 , "--builders", nixBuildersArgString stateDir
161161 ]
162162 unless (exitCode == ExitSuccess) $ do
lib/executable-config/default.nix+6−3
@@ -4,7 +4,7 @@
44# platform.
55{ lib
66, runCommand
7−, cleanHaskellSource
7+, obeliskCleanSource
88}:
99
1010let
@@ -23,7 +23,10 @@ let
2323in
2424
2525{
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+
2730 platforms = {
2831 android = {
2932 # Inject the given config directory into an android assets folder
@@ -34,7 +37,7 @@ in
3437 inject = injectConfig;
3538 };
3639 web = {
37− inject = self: self.callCabal2nix "obelisk-executable-config-inject" (cleanHaskellSource ./inject) {};
40+ inject = self: self.callCabal2nix "obelisk-executable-config-inject" (obeliskCleanSource ./inject) {};
3841 };
3942 };
4043}
lib/executable-config/inject/obelisk-executable-config-inject.cabal+2−2
@@ -9,8 +9,8 @@ library
99 build-depends:
1010 base,
1111 bytestring,
12− directory,
13− filepath,
12+ base64-bytestring,
13+ containers,
1414 reflex-dom-core,
1515 text
1616
lib/executable-config/inject/src/Obelisk/ExecutableConfig/Inject.hs+14−40
@@ -2,50 +2,24 @@
22module Obelisk.ExecutableConfig.Inject where
33
44import Control.Monad (mapM_)
5−import Control.Monad.IO.Class (MonadIO, liftIO)
6−import Data.Semigroup ((<>))
5+import Control.Monad.IO.Class (MonadIO)
76import 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 ((<>))
811import Data.Text (Text)
9−import qualified Data.Text as T
10−import qualified Data.Text.IO as T
12+import qualified Data.Text.Encoding as T
1113import 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
2114
2215-- | Produces a @<script>@ tag with an @id@ attribute containing the key and
23−-- whose value is the provided configuration
24−injectPure :: DomBuilder t m => Text -> Text -> m ()
16+-- whose value is the provided configuration, encoded in base64.
17+injectPure :: DomBuilder t m => Text -> ByteString -> m ()
2518injectPure key value =
26− let attrs = ("type" =: "text/plain" <> "id" =: ("config-" <> key))
27− in elAttr "script" attrs $ text 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)
19+ let attrs = ("type" =: "text/plain" <> "data-obelisk-executable-config-inject-key" =: key <> "data-hydration-skip" =: "")
20+ in elAttr "script" attrs $ text (T.decodeUtf8 (B64.encode value))
3721
38−getConfigs :: MonadIO m => FilePath -> m [(Text, Text)]
39−getConfigs fp = liftIO $ do
40− dir <- doesDirectoryExist fp
41− if dir
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 []
22+-- | Produces injectable @<script>@ tags containing the configuration keys
23+-- (filepaths) and values.
24+injectExecutableConfigs :: (MonadIO m, DomBuilder t m) => Map Text ByteString -> m ()
25+injectExecutableConfigs = mapM_ (uncurry injectPure) . Map.toList
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
77 hs-source-dirs: src
88 build-depends: base,
99 bytestring,
10+ containers,
11+ cookie,
1012 dependent-sum,
1113 ghcjs-dom,
1214 jsaddle,
1315 lens,
16+ mtl,
17+ obelisk-executable-config-inject,
18+ obelisk-executable-config-lookup,
1419 obelisk-route,
1520 primitive,
1621 ref-tf,
@@ -18,5 +23,7 @@ library
1823 reflex-dom-core,
1924 text,
2025 transformers
21− exposed-modules: Obelisk.Frontend
26+ exposed-modules:
27+ Obelisk.Frontend
28+ Obelisk.Frontend.Cookie
2229 ghc-options: -Wall
lib/frontend/src/Obelisk/Frontend.hs+182−96
@@ -1,10 +1,13 @@
1+{-# LANGUAGE CPP #-}
12{-# LANGUAGE ConstraintKinds #-}
23{-# LANGUAGE FlexibleContexts #-}
34{-# LANGUAGE FlexibleInstances #-}
45{-# LANGUAGE LambdaCase #-}
56{-# LANGUAGE MultiParamTypeClasses #-}
67{-# LANGUAGE OverloadedStrings #-}
8+{-# LANGUAGE PackageImports #-}
79{-# LANGUAGE RankNTypes #-}
10+{-# LANGUAGE RecursiveDo #-}
811{-# LANGUAGE ScopedTypeVariables #-}
912{-# LANGUAGE TemplateHaskell #-}
1013{-# LANGUAGE TypeFamilies #-}
@@ -13,39 +16,55 @@ module Obelisk.Frontend
1316 ( ObeliskWidget
1417 , Frontend (..)
1518 , runFrontend
19+ , runFrontendWithConfigsAndCurrentRoute
1620 , renderFrontendHtml
21+ , removeHTMLConfigs
22+ , FrontendMode (..)
23+ , FrontendWidgetT
24+ , module Obelisk.Frontend.Cookie
1725 ) where
1826
1927import Prelude hiding ((.))
2028
2129import Control.Category
22−import Control.Concurrent
2330import Control.Lens
24−import Control.Monad
2531import Control.Monad.Fix
2632import Control.Monad.IO.Class
2733import Control.Monad.Primitive
34+import Control.Monad.Reader
2835import Control.Monad.Ref
29−import Control.Monad.Trans.Class
36+import Control.Monad.Trans.Maybe (MaybeT(..), runMaybeT)
3037import Data.ByteString (ByteString)
31−import Data.Dependent.Sum (DSum (..))
38+import Data.Foldable (for_)
3239import Data.Functor.Sum
33−import Data.IORef
40+import Data.Map (Map)
41+import Data.Maybe (catMaybes)
3442import Data.Monoid ((<>))
3543import Data.Text (Text)
36−import GHCJS.DOM hiding (bracket, catch)
37−import GHCJS.DOM.Document
38−import GHCJS.DOM.Node
44+import qualified GHCJS.DOM as DOM
3945import 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
4155import Obelisk.Route.Frontend
4256import Reflex.Dom.Core
4357import 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
4564
4665makePrisms ''Sum
4766
48−type ObeliskWidget t x route m =
67+type ObeliskWidget js t route m =
4968 ( DomBuilder t m
5069 , MonadFix m
5170 , MonadHold t m
@@ -53,8 +72,6 @@ type ObeliskWidget t x route m =
5372 , MonadReflexCreateTrigger t m
5473 , PostBuild t m
5574 , PerformEvent t m
56− , MonadIO m
57− , MonadIO (Performable m)
5875 , TriggerEvent t m
5976 , HasDocument m
6077 , MonadRef m
@@ -63,106 +80,175 @@ type ObeliskWidget t x route m =
6380 , Ref (Performable m) ~ Ref IO
6481 , MonadFix (Performable m)
6582 , PrimMonad m
66− , Prerender x m
67− , SetRoute t route m
83+ , Prerender js t 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
6893 , RouteToUrl route m
94+ , MonadFix m
95+ , HasConfigs m
96+ , HasConfigs (Performable m)
6997 )
7098
7199data Frontend route = Frontend
72− { _frontend_head :: !(forall t m x. ObeliskWidget t x route m => RoutedT t route m ())
73− , _frontend_body :: !(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 ())
101+ , _frontend_body :: !(forall js t m. ObeliskWidget js t route m => RoutedT t route m ())
74102 }
75103
76−type Widget' x = ImmediateDomBuilderT DomTimeline (DomCoreWidget x)
77−
78−-- | A widget that isn't attached to any particular part of the DOM hierarchy
79−type FloatingWidget x = TriggerEventT DomTimeline (DomCoreWidget x)
80−
81−type DomCoreWidget x = PostBuildT DomTimeline (WithJSContextSingleton x (PerformEventT DomTimeline DomHost))
82−
83−--TODO: Rename
84−{-# INLINABLE attachWidget''' #-}
85−attachWidget'''
86− :: (EventChannel -> PerformEventT DomTimeline DomHost (IORef (Maybe (EventTrigger DomTimeline ()))))
87− -> IO ()
88−attachWidget''' w = runDomHost $ do
89− events <- liftIO newChan
90− (postBuildTriggerRef, fc@(FireCommand fire)) <- hostPerformEventT $ w events
91− mPostBuildTrigger <- readRef postBuildTriggerRef
92− forM_ mPostBuildTrigger $ \postBuildTrigger -> fire [postBuildTrigger :=> Identity ()] $ return ()
93− liftIO $ processAsyncEvents events fc
94−
95−--TODO: This is a collection of random stuff; we should make it make some more sense and then upstream to reflex-dom-core
96−runWithHeadAndBody
97− :: ( (forall c. Widget' () c -> FloatingWidget () c) -- "Append to head"
98− -> (forall c. Widget' () c -> FloatingWidget () c) -- "Append to body"
99− -> FloatingWidget () ()
100− )
104+baseTag :: forall route js t m. ObeliskWidget js t route m => RoutedT t route m ()
105+baseTag = elAttr "base" ("href" =: "/") blank --TODO: Figure out the base URL from the routes
106+
107+removeHTMLConfigs :: JSM ()
108+removeHTMLConfigs = void $ runMaybeT $ do
109+ doc <- MaybeT currentDocument
110+ hd <- MaybeT $ getHead doc
111+ es <- nodeListNodes =<< querySelectorAll hd ("[data-obelisk-executable-config-inject-key]" :: Text)
112+ for_ es $ removeChild_ hd
113+
114+-- | Collect all nodes in the node list.
115+--
116+-- TODO: this and the version in exe-config/ghcjs/lookup should be
117+-- upstreamed to jsaddle.
118+nodeListNodes :: (IsNodeList l, MonadJSM m) => l -> m [Node]
119+nodeListNodes es = do
120+ len <- getLength es
121+ -- Warning! len is unsigned. If the NodeList is empty, we must avoid
122+ -- accidentally traversing over [0..maxBound::Word]
123+ nodes <- traverse (item es) $ if len == 0 then [] else [0..len-1]
124+ pure $ catMaybes nodes
125+
126+setInitialRoute :: Bool -> JSM ()
127+setInitialRoute useHash = do
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)
101152 -> 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 ()
134153runFrontend 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))
136186 errorLeft = \case
137187 Left _ -> error "runFrontend: Unexpected non-app ObeliskRoute reached the frontend. This shouldn't happen."
138188 Right x -> Identity x
139− runMyRouteViewT
140− :: ( TriggerEvent t m
141− , PerformEvent t m
142− , MonadHold t m
189+ w :: ( RawDocument (DomBuilderSpace (HydrationDomBuilderT s DomTimeline m)) ~ DOM.Document
190+ , Ref (Performable m) ~ Ref IO
191+ , Ref m ~ Ref IO
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)
143201 , DOM.MonadJSM m
144− , DOM.MonadJSM (Performable m)
145− , MonadFix m
202+ , MonadFix (Client (HydrationDomBuilderT s DomTimeline m))
146203 , MonadFix (Performable m)
204+ , MonadFix m
205+ , Prerender js DomTimeline (HydrationDomBuilderT s DomTimeline m)
206+ , MonadIO (Performable m)
147207 )
148− => RoutedT t (R route) (SetRouteT t (R route) (RouteToUrlT (R route) m)) a
149− -> m a
150− runMyRouteViewT = runRouteViewT ve
151− runWithHeadAndBody $ \appendHead appendBody -> runMyRouteViewT $ do
152− mapRoutedT (mapSetRouteT (mapRouteToUrlT appendHead)) $ _frontend_head frontend
153− mapRoutedT (mapSetRouteT (mapRouteToUrlT appendBody)) $ _frontend_body frontend
208+ => (forall c. HydrationDomBuilderT s DomTimeline m c -> FloatingWidget () c)
209+ -> (forall c. HydrationDomBuilderT s DomTimeline m c -> FloatingWidget () c)
210+ -> FloatingWidget () ()
211+ w appendHead appendBody = do
212+ rec switchover <- runRouteViewT ve switchover (_frontendMode_adjustRoute mode) $ do
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))))))))
154231
155232renderFrontendHtml
156− :: (t ~ DomTimeline)
157− => (r' -> Text)
233+ :: MonadIO m
234+ => Map Text ByteString
235+ -> Cookies
236+ -> (r -> Text)
158237 -> r
159− -> RoutedT t r (SetRouteT t r' (RouteToUrlT r' (PostBuildT DomTimeline (StaticDomBuilderT DomTimeline (PerformEventT DomTimeline DomHost))))) ()
160− -> RoutedT t r (SetRouteT t r' (RouteToUrlT r' (PostBuildT DomTimeline (StaticDomBuilderT DomTimeline (PerformEventT DomTimeline DomHost))))) ()
161− -> IO ByteString
162−renderFrontendHtml urlEnc route headWidget bodyWidget = do
238+ -> Frontend r
239+ -> FrontendWidgetT r ()
240+ -> FrontendWidgetT r ()
241+ -> m ByteString
242+renderFrontendHtml configs cookies urlEnc route frontend headExtra bodyExtra = do
163243 --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) $
165245 el "html" $ do
166− el "head" headWidget
167− el "body" bodyWidget
246+ el "head" $ do
247+ baseTag
248+ injectExecutableConfigs configs
249+ _frontend_head frontend
250+ headExtra
251+ el "body" $ do
252+ _frontend_body frontend
253+ bodyExtra
168254 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
66library
77 hs-source-dirs: src
88 build-depends: base,
9+ aeson,
10+ bytestring,
911 categories,
1012 constraints,
13+ constraints-extras,
1114 containers,
1215 dependent-map,
1316 dependent-sum,
14− dependent-sum-universe-orphans,
1517 dependent-sum-template,
1618 either,
1719 ghcjs-dom,
@@ -21,15 +23,18 @@ library
2123 monad-control,
2224 mtl,
2325 network-uri,
26+ obelisk-executable-config-lookup,
2427 primitive,
2528 ref-tf,
2629 reflex,
2730 reflex-dom-core,
2831 template-haskell,
32+ tabulation,
2933 text,
34+ th-extras,
3035 transformers,
3136 universe,
32− universe-template
37+ universe-dependent-sum
3338 exposed-modules: Obelisk.Route
3439 Obelisk.Route.TH
3540 Obelisk.Route.Frontend
lib/route/src/Obelisk/Route.hs+395−40
@@ -12,12 +12,18 @@
1212{-# LANGUAGE PatternSynonyms #-}
1313{-# LANGUAGE RankNTypes #-}
1414{-# LANGUAGE ScopedTypeVariables #-}
15+{-# LANGUAGE StandaloneDeriving #-}
1516{-# LANGUAGE TemplateHaskell #-}
17+{-# LANGUAGE TypeApplications #-}
1618{-# LANGUAGE TypeFamilies #-}
1719{-# LANGUAGE TypeOperators #-}
1820{-# LANGUAGE ViewPatterns #-}
1921module Obelisk.Route
2022 ( R
23+ , (:.)
24+ , (?/)
25+ , hoistR
26+ , pattern (:.)
2127 , pattern (:/)
2228 , PageName
2329 , PathQuery
@@ -40,6 +46,9 @@ module Obelisk.Route
4046 , checkEnum1EncoderFunc
4147 , unitEncoder
4248 , pathOnlyEncoder
49+ , addPathSegmentEncoder
50+ , pathParamEncoder
51+ , pathLiteralEncoder
4352 , singletonListEncoder
4453 , unpackTextEncoder
4554 , prefixTextEncoder
@@ -60,10 +69,15 @@ module Obelisk.Route
6069 , shadowEncoder
6170 , prismEncoder
6271 , rPrism
72+ , _R
6373 , obeliskRouteEncoder
6474 , obeliskRouteSegment
6575 , pageNameEncoder
6676 , handleEncoder
77+ , FullRoute (..)
78+ , _FullRoute_Frontend
79+ , _FullRoute_Backend
80+ , mkFullRouteEncoder
6781 , ObeliskRoute (..)
6882 , _ObeliskRoute_App
6983 , _ObeliskRoute_Resource
@@ -78,6 +92,19 @@ module Obelisk.Route
7892 , void1Encoder
7993 , pathSegmentsTextEncoder
8094 , 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
81108 ) where
82109
83110import Prelude hiding ((.), id)
@@ -88,8 +115,32 @@ import qualified Control.Categorical.Functor as Cat
88115import Control.Categorical.Bifunctor
89116import Control.Category.Associative
90117import 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+ )
92138import 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
93144import Data.Dependent.Sum (DSum (..))
94145import Data.Dependent.Map (DMap)
95146import qualified Data.Dependent.Map as DMap
@@ -106,16 +157,19 @@ import Data.Maybe
106157import Data.Monoid ((<>))
107158import Data.Set (Set)
108159import qualified Data.Set as Set
109−import Data.Some (Some)
110−import qualified Data.Some as Some
111−import Data.Some.Universe.Orphans ()
160+import Data.Some (Some(Some))
112161import Data.Text (Text)
113162import qualified Data.Text as T
114−import Data.Text.Encoding
163+import qualified Data.Text.Encoding as T
115164import Data.Universe
165+import Data.Universe.Some
116166import Network.HTTP.Types.URI
167+import qualified Numeric.Lens
117168import Obelisk.Route.TH
118169import Text.Read (readMaybe)
170+import Data.Tabulation
171+import qualified Data.Aeson as Aeson
172+import Data.Aeson (FromJSON, ToJSON)
119173
120174-- Design goals:
121175-- No start-up time on the frontend (not yet met)
@@ -141,13 +195,64 @@ import Text.Read (readMaybe)
141195
142196type R f = DSum f Identity --TODO: Better name
143197
144−--TODO: COMPLETE pragma
145−infixr 5 :/
198+-- | Convenience builder for an 'R' using 'Identity' for the functor.
146199pattern (:/) :: f a -> a -> R f
147200pattern 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 ?/
148208
149209mapSome :: (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
151256
152257--------------------------------------------------------------------------------
153258-- Encoder fundamentals
@@ -182,7 +287,7 @@ decode e x = runIdentity (tryDecode e x)
182287tryDecode :: Encoder Identity parse decoded encoded -> encoded -> parse decoded
183288tryDecode (Encoder (Identity impl)) x = _encoderImpl_decode impl x
184289
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
186291-- can be used to actually encode by using this. Note that while there's no constraint on the parse monad here,
187292-- one should usually be applying decode and encode to the same 'Encoder'
188293encode :: Encoder Identity parse decoded encoded -> decoded -> encoded
@@ -232,6 +337,10 @@ instance Monad parse => Bifunctor (,) (EncoderImpl parse) (EncoderImpl parse) (E
232337 , _encoderImpl_decode = \(a, b) -> liftA2 (,) (_encoderImpl_decode f a) (_encoderImpl_decode g b)
233338 }
234339
340+instance (Monad parse, Applicative check) => Braided (Encoder check parse) (,) where
341+ braid = isoEncoder (iso swap swap)
342+
343+
235344instance (Applicative check, Monad parse) => PFunctor (,) (Encoder check parse) (Encoder check parse) where
236345 first f = bimap f id
237346instance (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
245354 , _encoderImpl_decode = traverse $ _encoderImpl_decode ve
246355 }
247356
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+
248385instance (Traversable f, Monad check, Monad parse) => Cat.Functor f (Encoder check parse) (Encoder check parse) where
249386 fmap e = Encoder $ do
250387 ve <- unEncoder e
@@ -332,14 +469,14 @@ maybeEncoder f g = shadowEncoder f g . maybeToEitherEncoder
332469justEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse a (Maybe a)
333470justEncoder = prismEncoder _Just
334471
335−-- |
472+-- | Encode () to 'Nothing'.
336473nothingEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse () (Maybe a)
337474nothingEncoder = prismEncoder _Nothing
338475
339476someConstEncoder :: (Applicative check, Applicative parse) => Encoder check parse (Some (Const a)) a
340477someConstEncoder = unsafeMkEncoder $ EncoderImpl
341− { _encoderImpl_encode = \(Some.This (Const a)) -> a
342− , _encoderImpl_decode = pure . Some.This . Const
478+ { _encoderImpl_encode = \(Some (Const a)) -> a
479+ , _encoderImpl_decode = pure . Some . Const
343480 }
344481
345482-- | WARNING: This is only safe if the Show and Read instances for 'a' are
@@ -367,7 +504,7 @@ checkEnum1EncoderFunc
367504 -> check (EncoderFunc check' parse p r)
368505checkEnum1EncoderFunc f = do
369506 (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
371508 pure $ EncoderFunc $ \p -> unsafeMkEncoder . unFlip $
372509 DMap.findWithDefault (error "checkEnum1EncoderFunc: EncoderImpl not found (should be impossible)") p encoderImpls
373510
@@ -422,11 +559,12 @@ chainEncoder cons this rest = Encoder $ do
422559 pure $ EncoderImpl
423560 { _encoderImpl_decode = \v -> do
424561 (here, following) <- _encoderImpl_decode consValid v
425− Some.This r <- _encoderImpl_decode thisValid here
426− (r :/) <$> _encoderImpl_decode (runIdentity . unEncoder $ rest r) following
562+ _encoderImpl_decode thisValid here >>= \case
563+ Some r ->
564+ (r :/) <$> _encoderImpl_decode (runIdentity . unEncoder $ rest r) following
427565 , _encoderImpl_encode = \(r :/ s) ->
428566 _encoderImpl_encode consValid
429− ( _encoderImpl_encode thisValid $ Some.This r
567+ ( _encoderImpl_encode thisValid $ Some r
430568 , _encoderImpl_encode (runIdentity . unEncoder $ rest r) s)
431569 }
432570
@@ -493,7 +631,7 @@ enum1Encoder
493631 , Show r
494632 )
495633 => (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
497635
498636-- | Encode an enumerable, bounded type. WARNING: Don't use this on types that
499637-- have a large number of values - it will use a lot of memory.
@@ -536,13 +674,10 @@ pathOnlyEncoderIgnoringQuery = unsafeMkEncoder $ EncoderImpl
536674 }
537675
538676pathOnlyEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse [Text] PageName
539−pathOnlyEncoder = unsafeMkEncoder $ EncoderImpl
540− { _encoderImpl_decode = \(path, query) ->
541− if query == mempty
542− then pure path
543− else throwError "pathOnlyEncoderImpl: query was provided"
544− , _encoderImpl_encode = \path -> (path, mempty)
545− }
677+pathOnlyEncoder = second (unitEncoder mempty) . coidr
678+
679+queryOnlyEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse (Map Text (Maybe Text)) PageName
680+queryOnlyEncoder = first (unitEncoder []) . coidl
546681
547682singletonListEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse a [a]
548683singletonListEncoder = unsafeMkEncoder $ EncoderImpl
@@ -585,10 +720,10 @@ queryParametersTextEncoder = Encoder $ pure $ EncoderImpl
585720 in (urlDecodeText True k, urlDecodeText True <$> mv)
586721
587722urlEncodeText :: Bool -> Text -> Text
588−urlEncodeText q = decodeUtf8 . urlEncode q . encodeUtf8
723+urlEncodeText q = T.decodeUtf8 . urlEncode q . T.encodeUtf8
589724
590725urlDecodeText :: Bool -> Text -> Text
591−urlDecodeText q = decodeUtf8 . urlDecode q . encodeUtf8
726+urlDecodeText q = T.decodeUtf8 . urlDecode q . T.encodeUtf8
592727
593728listToNonEmptyEncoder :: (Applicative check, Applicative parse, Monoid a, Eq a) => Encoder check parse [a] (NonEmpty a)
594729listToNonEmptyEncoder = Encoder $ pure $ EncoderImpl
@@ -646,10 +781,47 @@ joinPairTextEncoder = Encoder . \case
646781 _ -> return (kt, T.drop (T.length separator) vt)
647782 }
648783
649−--TODO: Rewrite this by composing the given prism with a lens on the first element of the DSum
650−-- Or, more likely, the user can compose it themselves
651−rPrism :: forall f g. (forall a. Prism' (f a) (g a)) -> Prism' (R f) (R g)
652−rPrism p = prism (\(g :/ x) -> g ^. re p :/ x) (\(f :/ x) -> bimap (:/ x) (:/ x) $ matching p f)
784+-- This slight generalization of 'rPrism' happens to be enough to write all our
785+-- prism combinators so far.
786+dSumPrism
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
653825
654826-- | An encoder that only works on the items available via the prism. An error will be thrown in the parse monad
655827-- if the prism doesn't match.
@@ -693,6 +865,45 @@ handleEncoder recover e = Encoder $ do
693865-- Actual obelisk route info
694866--------------------------------------------------------------------------------
695867
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+
696907-- | A type which can represent Obelisk-specific resource routes, in addition to application specific routes which serve your
697908-- frontend.
698909data ObeliskRoute :: (* -> *) -> * -> * where
@@ -700,9 +911,11 @@ data ObeliskRoute :: (* -> *) -> * -> * where
700911 ObeliskRoute_App :: f a -> ObeliskRoute f a
701912 ObeliskRoute_Resource :: ResourceRoute a -> ObeliskRoute f a
702913
703−instance Universe (Some f) => Universe (Some (ObeliskRoute f)) where
704− universe = fmap (\(Some.This x) -> Some.This (ObeliskRoute_App x)) universe
705− ++ fmap (\(Some.This x) -> Some.This (ObeliskRoute_Resource x)) universe
914+instance UniverseSome f => UniverseSome (ObeliskRoute f) where
915+ universeSome = concat
916+ [ (\(Some x) -> Some (ObeliskRoute_App x)) <$> universe
917+ , (\(Some x) -> Some (ObeliskRoute_Resource x)) <$> universe
918+ ]
706919
707920instance GEq f => GEq (ObeliskRoute f) where
708921 geq (ObeliskRoute_App x) (ObeliskRoute_App y) = geq x y
@@ -790,12 +1003,12 @@ indexOnlyRouteEncoder = pathComponentEncoder indexOnlyRouteSegment
7901003
7911004someSumEncoder :: (Applicative check, Applicative parse) => Encoder check parse (Some (Sum a b)) (Either (Some a) (Some b))
7921005someSumEncoder = Encoder $ pure $ EncoderImpl
793− { _encoderImpl_encode = \(Some.This t) -> case t of
794− InL l -> Left $ Some.This l
795− InR r -> Right $ Some.This r
1006+ { _encoderImpl_encode = \(Some t) -> case t of
1007+ InL l -> Left $ Some l
1008+ InR r -> Right $ Some r
7961009 , _encoderImpl_decode = pure . \case
797− Left (Some.This l) -> Some.This (InL l)
798− Right (Some.This r) -> Some.This (InR r)
1010+ Left (Some l) -> Some (InL l)
1011+ Right (Some r) -> Some (InR r)
7991012 }
8001013
8011014data Void1 :: * -> * where {}
@@ -806,7 +1019,7 @@ instance Universe (Some Void1) where
8061019void1Encoder :: (Applicative check, MonadError Text parse) => Encoder check parse (Some Void1) a
8071020void1Encoder = Encoder $ pure $ EncoderImpl
8081021 { _encoderImpl_encode = \case
809− Some.This f -> case f of {}
1022+ Some f -> case f of {}
8101023 , _encoderImpl_decode = \_ -> throwError "void1Encoder: can't decode anything"
8111024 }
8121025
@@ -820,8 +1033,150 @@ concat <$> mapM deriveRouteComponent
8201033 ]
8211034
8221035makePrisms ''ObeliskRoute
823−
1036+makePrisms ''FullRoute
8241037deriveGEq ''Void1
8251038deriveGCompare ''Void1
8261039
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+
8271182--TODO: decodeURIComponent as appropriate
lib/route/src/Obelisk/Route/Frontend.hs+226−51
@@ -3,17 +3,15 @@
33{-# LANGUAGE RecursiveDo #-}
44{-# LANGUAGE StandaloneDeriving #-}
55{-# LANGUAGE ConstraintKinds #-}
6+{-# LANGUAGE DefaultSignatures #-}
67{-# LANGUAGE FlexibleContexts #-}
78{-# LANGUAGE FlexibleInstances #-}
89{-# LANGUAGE GADTs #-}
9−{-# LANGUAGE KindSignatures #-}
1010{-# LANGUAGE LambdaCase #-}
11−{-# LANGUAGE MultiParamTypeClasses #-}
1211{-# LANGUAGE OverloadedStrings #-}
1312{-# LANGUAGE PatternSynonyms #-}
1413{-# LANGUAGE RankNTypes #-}
1514{-# LANGUAGE ScopedTypeVariables #-}
16−{-# LANGUAGE TemplateHaskell #-}
1715{-# LANGUAGE TypeFamilies #-}
1816{-# LANGUAGE TypeOperators #-}
1917{-# LANGUAGE ViewPatterns #-}
@@ -33,9 +31,14 @@ module Obelisk.Route.Frontend
3331 , mapRoutedT
3432 , subRoute
3533 , subRoute_
34+ , subPairRoute
35+ , subPairRoute_
3636 , maybeRoute
3737 , maybeRoute_
3838 , maybeRouted
39+ , eitherRoute
40+ , eitherRoute_
41+ , eitherRouted
3942 , runRouteViewT
4043 , SetRouteT(..)
4144 , SetRoute(..)
@@ -46,6 +49,10 @@ module Obelisk.Route.Frontend
4649 , runRouteToUrlT
4750 , mapRouteToUrlT
4851 , routeLink
52+ , routeLinkDynAttr
53+ , dynRouteLink
54+ , adaptedUriPath
55+ , setAdaptedUriPath
4956 ) where
5057
5158import Prelude hiding ((.), id)
@@ -53,23 +60,24 @@ import Prelude hiding ((.), id)
5360import Obelisk.Route
5461
5562import Control.Category (Category (..), (.))
56−import Control.Category.Cartesian
63+import Control.Category.Cartesian ((&&&))
5764import Control.Lens hiding (Bifunctor, bimap, universe, element)
65+import Control.Monad ((<=<))
5866import Control.Monad.Fix
5967import Control.Monad.Primitive
68+import Control.Monad.Reader
6069import Control.Monad.Ref
61−import Control.Monad.Trans
6270import Control.Monad.Trans.Control
63−import Control.Monad.Trans.Reader
6471import Data.Coerce
65−import Data.Constraint (Dict (..))
6672import Data.Dependent.Sum (DSum (..))
6773import Data.GADT.Compare
74+import Data.Map (Map)
6875import Data.Monoid
6976import Data.Proxy
7077import Data.Text (Text)
7178import qualified Data.Text as T
7279import Data.Functor.Compose
80+import Data.Functor.Misc
7381import Reflex.Class
7482import Reflex.Host.Class
7583import Reflex.PostBuild.Class
@@ -82,34 +90,47 @@ import Reflex.Dom.Builder.Class
8290import Data.Type.Coercion
8391import Language.Javascript.JSaddle --TODO: Get rid of this - other platforms can also be routed
8492import Reflex.Dom.Core
93+import qualified GHCJS.DOM as DOM
8594import qualified GHCJS.DOM.Types as DOM
95+import qualified GHCJS.DOM.Window as Window
8696import Network.URI
87−#if defined(ios_HOST_OS)
8897import Data.Maybe (fromMaybe)
8998import qualified Data.List as L
90−#endif
9199
92100import Unsafe.Coerce
93101
102+import Obelisk.Configs
103+
94104infixr 5 :~
95105pattern (:~) :: Reflex t => f a -> Dynamic t a -> DSum f (Compose (Dynamic t) Identity)
96106pattern a :~ b <- a :=> (coerceDynamic . getCompose -> b)
97107
98108class Routed t r m | m -> t r where
99109 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
100112
101113instance Monad m => Routed t r (RoutedT t r m) where
102114 askRoute = RoutedT ask
103115
116+instance (Monad m, Routed t r m) => Routed t r (ReaderT r' m)
117+
104118newtype 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
106124
107125instance HasJSContext m => HasJSContext (RoutedT t r m) where
108126 type JSContextPhantom (RoutedT t r m) = JSContextPhantom m
109127 askJSContext = lift askJSContext
110128
111−instance Prerender js m => Prerender js (RoutedT t r m) where
112− prerenderClientDict = fmap (\Dict -> Dict) (prerenderClientDict :: Maybe (Dict (PrerenderClientConstraint js m)))
129+instance (Prerender js t m, Monad m) => Prerender js t (RoutedT t r m) where
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)
113134
114135instance Requester t m => Requester t (RoutedT t r m) where
115136 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
161182 askQueryResult = lift askQueryResult
162183 queryIncremental = lift . queryIncremental
163184
164−instance (Monad m, RouteToUrl r m) => RouteToUrl r (QueryT t q m) where
165− askRouteToUrl = lift askRouteToUrl
185+instance HasConfigs m => HasConfigs (RoutedT t r m)
166186
167−instance (Monad m, SetRoute t r m) => SetRoute t r (QueryT t q m) where
168− setRoute = lift . setRoute
169− modifyRoute = lift . modifyRoute
187+instance (Monad m, RouteToUrl r m) => RouteToUrl r (QueryT t q m)
188+
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)
170194
171195runRoutedT :: RoutedT t r m a -> Dynamic t r -> m a
172196runRoutedT = runReaderT . unRoutedT
173197
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
175199mapRoutedT f = RoutedT . mapReaderT f . unRoutedT
176200
177201withRoutedT :: (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
181205subRoute_ f = factorRouted $ strictDynWidget_ $ \(c :=> r') -> do
182206 runRoutedT (f c) r'
183207
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+
184212subRoute :: (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)
185213subRoute f = factorRouted $ strictDynWidget $ \(c :=> r') -> do
186214 runRoutedT (f c) r'
187215
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+
188220maybeRoute_ :: (MonadFix m, MonadHold t m, Adjustable t m) => m () -> RoutedT t r m () -> RoutedT t (Maybe r) m ()
189221maybeRoute_ n j = maybeRouted $ strictDynWidget_ $ \case
190222 Nothing -> n
@@ -201,6 +233,20 @@ maybeRoute f = factorRouted $ strictDynWidget $ \(c :=> r') -> do
201233 runRoutedT (f c) r'
202234-}
203235
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+
204250dsumValueCoercion :: Coercion f g -> Coercion (DSum k f) (DSum k g)
205251dsumValueCoercion Coercion = Coercion
206252
@@ -217,6 +263,9 @@ maybeRouted r = RoutedT $ ReaderT $ \d -> do
217263 d' <- maybeDyn d
218264 runRoutedT r d'
219265
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+
220269-- | WARNING: The input 'Dynamic' must be fully constructed when this is run
221270strictDynWidget :: (MonadSample t m, MonadHold t m, Adjustable t m) => (a -> m b) -> RoutedT t a m (Dynamic t b)
222271strictDynWidget f = RoutedT $ ReaderT $ \r -> do
@@ -231,7 +280,7 @@ strictDynWidget_ f = RoutedT $ ReaderT $ \r -> do
231280 pure ()
232281
233282newtype 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)
235284
236285instance (MonadFix m, MonadHold t m, DomBuilder t m) => DomBuilder t (SetRouteT t r m) where
237286 type DomBuilderSpace (SetRouteT t r m) = DomBuilderSpace m
@@ -253,16 +302,26 @@ runSetRouteT = runEventWriterT . unSetRouteT
253302class Reflex t => SetRoute t r m | m -> t r where
254303 setRoute :: Event t r -> m ()
255304 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+
256308 setRoute = modifyRoute . fmap const
257309
258310instance (Reflex t, Monad m) => SetRoute t r (SetRouteT t r m) where
259311 modifyRoute = SetRouteT . tellEvent . fmap Endo
260312
261−instance (Monad m, SetRoute t r m) => SetRoute t r (RoutedT t r' m) where
262− modifyRoute = lift . modifyRoute
313+instance (Monad m, SetRoute t r m) => SetRoute t r (RoutedT t r' m)
263314
264−instance Prerender js m => Prerender js (SetRouteT t r m) where
265− prerenderClientDict = fmap (\Dict -> Dict) (prerenderClientDict :: Maybe (Dict (PrerenderClientConstraint js m)))
315+instance (Monad m, SetRoute t r m) => SetRoute t r (ReaderT r' 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
266325
267326instance Requester t m => Requester t (SetRouteT t r m) where
268327 type Request (SetRouteT t r m) = Request m
@@ -270,6 +329,8 @@ instance Requester t m => Requester t (SetRouteT t r m) where
270329 requesting = SetRouteT . requesting
271330 requesting_ = SetRouteT . requesting_
272331
332+instance (Monad m, SetRoute t r m) => SetRoute t r (RequesterT t req rsp m)
333+
273334#ifndef ghcjs_HOST_OS
274335deriving instance MonadJSM m => MonadJSM (SetRouteT t r m)
275336#endif
@@ -293,6 +354,8 @@ instance PrimMonad m => PrimMonad (SetRouteT t r m ) where
293354 type PrimState (SetRouteT t r m) = PrimState m
294355 primitive = lift . primitive
295356
357+instance HasConfigs m => HasConfigs (SetRouteT t r m)
358+
296359instance (MonadHold t m, Adjustable t m) => Adjustable t (SetRouteT t r m) where
297360 runWithReplace a0 a' = SetRouteT $ runWithReplace (coerce a0) $ coerceEvent a'
298361 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
306369
307370class RouteToUrl r m | m -> r where
308371 askRouteToUrl :: m (r -> Text)
372+ default askRouteToUrl :: (Monad m', MonadTrans f, RouteToUrl r m', m ~ f m') => m (r -> Text)
373+ askRouteToUrl = lift askRouteToUrl
309374
310375newtype 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)
312377
313378runRouteToUrlT
314379 :: RouteToUrlT r m a
@@ -323,17 +388,22 @@ instance Monad m => RouteToUrl r (RouteToUrlT r m) where
323388 askRouteToUrl = RouteToUrlT ask
324389
325390instance (Monad m, RouteToUrl r m) => RouteToUrl r (SetRouteT t r' m) where
326− askRouteToUrl = lift askRouteToUrl
327391
328392instance (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)
330397
331398instance HasJSContext m => HasJSContext (RouteToUrlT r m) where
332399 type JSContextPhantom (RouteToUrlT r m) = JSContextPhantom m
333400 askJSContext = lift askJSContext
334401
335−instance Prerender js m => Prerender js (RouteToUrlT r m) where
336− prerenderClientDict = fmap (\Dict -> Dict) (prerenderClientDict :: Maybe (Dict (PrerenderClientConstraint js m)))
402+instance (Prerender js t m, Monad m) => Prerender js t (RouteToUrlT r m) where
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)
337407
338408instance Requester t m => Requester t (RouteToUrlT r m) where
339409 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
383453 askQueryResult = lift askQueryResult
384454 queryIncremental = lift . queryIncremental
385455
456+instance HasConfigs m => HasConfigs (RouteToUrlT t m)
457+
386458runRouteViewT
387459 :: forall t m r a.
388460 ( TriggerEvent t m
@@ -393,16 +465,20 @@ runRouteViewT
393465 , MonadFix m
394466 )
395467 => (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
396472 -> RoutedT t r (SetRouteT t r (RouteToUrlT r m)) a
397473 -> m a
398−runRouteViewT routeEncoder a = do
399− rec historyState <- manageHistory $ HistoryCommand_PushState <$> setState
474+runRouteViewT routeEncoder switchover useHash a = do
475+ rec historyState <- manageHistory' switchover $ HistoryCommand_PushState <$> setState
400476 let theEncoder = pageNameEncoder . hoistParse (pure . runIdentity) routeEncoder
401477 -- NB: The can only fail if the uriPath doesn't begin with a '/' or if the uriQuery
402478 -- is nonempty, but begins with a character that isn't '?'. Since we don't expect
403479 -- this ever to happen, we'll just handle it by failing completely with 'error'.
404480 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
406482 where
407483 errorLeft (Left e) = error (T.unpack e)
408484 errorLeft (Right x) = x
@@ -423,7 +499,7 @@ runRouteViewT routeEncoder a = do
423499 -- we can change this function later to accommodate.
424500 -- See: https://github.com/whatwg/html/issues/2174
425501 , _historyStateUpdate_title = ""
426− , _historyStateUpdate_uri = Just $ setAdaptedUriPath newPath $ (_historyItem_uri currentHistoryState)
502+ , _historyStateUpdate_uri = Just $ setAdaptedUriPath useHash newPath $ (_historyItem_uri currentHistoryState)
427503 { uriQuery = newQuery
428504 }
429505 }
@@ -433,42 +509,141 @@ runRouteViewT routeEncoder a = do
433509-- | A link widget that, when clicked, sets the route to the provided route. In non-javascript
434510-- contexts, this widget falls back to using @href@s to control navigation
435511routeLink
436− :: forall t m a route.
512+ :: forall t m a route js.
437513 ( DomBuilder t m
438− , RouteToUrl (R route) m
439− , SetRoute t (R route) m
514+ , RouteToUrl route m
515+ , SetRoute t route m
516+ , Prerender js t m
440517 )
441− => R route -- ^ Target route
518+ => route -- ^ Target route
442519 -> m a -- ^ Child widget
443520 -> m a
444521routeLink 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
445537 enc <- askRouteToUrl
446538 let cfg = (def :: ElementConfig EventResult t (DomBuilderSpace m))
447539 & elementConfig_eventSpec %~ addEventSpecFlags (Proxy :: Proxy (DomBuilderSpace m)) Click (\_ -> preventDefault)
448540 & elementConfig_initialAttributes .~ "href" =: enc r
449541 (e, a) <- element "a" cfg w
450542 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
451607 return a
452608
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+
453632-- On ios due to sandboxing when loading the page from a file adapt the
454633-- path to be based on the hash.
455634
456−adaptedUriPath :: URI -> String
457−#if defined(ios_HOST_OS)
458−adaptedUriPath = hashToPath . uriFragment
459−
460−hashToPath :: String -> String
461−hashToPath = ('/' :) . fromMaybe "" . L.stripPrefix "#"
462−#else
463−adaptedUriPath = uriPath
464−#endif
635+adaptedUriPath :: Bool -> URI -> String
636+adaptedUriPath = \case
637+ True -> hashToPath . uriFragment
638+ False -> uriPath
465639
466−setAdaptedUriPath :: String -> URI -> URI
467−#if defined(ios_HOST_OS)
468−setAdaptedUriPath s u = u { uriFragment = pathToHash s }
640+setAdaptedUriPath :: Bool -> String -> URI -> URI
641+setAdaptedUriPath useHash s u = case useHash of
642+ True -> u { uriFragment = pathToHash s }
643+ False -> u { uriPath = s }
469644
470645pathToHash :: String -> String
471646pathToHash = ('#' :) . fromMaybe "" . L.stripPrefix "/"
472−#else
473−setAdaptedUriPath s u = u { uriPath = s }
474−#endif
647+
648+hashToPath :: String -> String
649+hashToPath = ('/' :) . fromMaybe "" . L.stripPrefix "#"
lib/route/src/Obelisk/Route/TH.hs+4−5
@@ -1,8 +1,9 @@
11module Obelisk.Route.TH (deriveRouteComponent) where
22
3+import Data.Constraint.Extras.TH
34import Data.GADT.Show.TH
45import Data.GADT.Compare.TH
5−import Data.Universe.TH
6+import Data.Universe.Some.TH
67import Language.Haskell.TH
78
89-- | 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)
1112 [ deriveGShow
1213 , deriveGEq
1314 , deriveGCompare
14− , deriveShowTagIdentity
15− , deriveEqTagIdentity
16− , deriveOrdTagIdentity
17− , deriveSomeUniverse
15+ , deriveUniverseSome
16+ , deriveArgDict
1817 ]
lib/run/obelisk-run.cabal+10−2
@@ -10,9 +10,12 @@ library
1010 attoparsec
1111 , base
1212 , bytestring
13+ , containers
14+ , cookie
1315 , dependent-sum
1416 , dependent-sum-template
1517 , ghcjs-dom
18+ , HsOpenSSL
1619 , http-client
1720 , http-reverse-proxy
1821 , http-types
@@ -24,8 +27,7 @@ library
2427 , network
2528 , obelisk-asset-serve-snap
2629 , obelisk-backend
27− , obelisk-executable-config
28− , obelisk-executable-config-inject
30+ , obelisk-executable-config-lookup
2931 , obelisk-frontend
3032 , obelisk-route
3133 , process
@@ -35,11 +37,17 @@ library
3537 , snap-core
3638 , streaming-commons
3739 , text
40+ , time
3841 , universe
42+ , utf8-string
3943 , wai
4044 , wai-websockets
4145 , warp
46+ , warp-tls
4247 , websockets
4348 exposed-modules:
4449 Obelisk.Run
4550 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+
16{-# LANGUAGE ConstraintKinds #-}
2−{-# LANGUAGE EmptyCase #-}
3−{-# LANGUAGE EmptyDataDecls #-}
47{-# LANGUAGE FlexibleContexts #-}
58{-# LANGUAGE FlexibleInstances #-}
69{-# LANGUAGE GADTs #-}
7−{-# LANGUAGE KindSignatures #-}
810{-# LANGUAGE LambdaCase #-}
911{-# LANGUAGE MultiParamTypeClasses #-}
1012{-# LANGUAGE OverloadedStrings #-}
1113{-# LANGUAGE Rank2Types #-}
12−{-# LANGUAGE RecursiveDo #-}
1314{-# LANGUAGE ScopedTypeVariables #-}
14−{-# LANGUAGE TemplateHaskell #-}
1515{-# LANGUAGE TypeFamilies #-}
1616{-# LANGUAGE UndecidableInstances #-}
1717{-# OPTIONS_GHC -fno-warn-orphans #-} -- Due to instance HasJS x (EventWriterT t w m)
@@ -25,17 +25,22 @@ import Control.Exception
2525import Control.Lens ((%~), (^?), _Just, _Right)
2626import qualified Data.Attoparsec.ByteString.Char8 as A
2727import Data.ByteString (ByteString)
28+import qualified Data.ByteString as BS
2829import qualified Data.ByteString.Char8 as BSC
2930import qualified Data.ByteString.Lazy.Char8 as BSLC
30−import Data.Dependent.Sum (DSum (..))
31+import qualified Data.ByteString.UTF8 as BSUTF8
3132import Data.Functor.Identity
32−import Data.Functor.Sum
3333import Data.List (uncons)
34+import Data.Map (Map)
35+import qualified Data.Map as Map
3436import Data.Maybe
3537import Data.Semigroup ((<>))
3638import Data.Streaming.Network (bindPortTCP)
39+import Data.String (fromString)
3740import Data.Text (Text)
3841import qualified Data.Text as T
42+import qualified Data.Text.Encoding as T
43+import Data.Time.Clock (getCurrentTime, addUTCTime)
3944import Language.Javascript.JSaddle.Run (syncPoint)
4045import Language.Javascript.JSaddle.WebSockets
4146import Network.HTTP.Client (Manager, defaultManagerSettings, newManager)
@@ -45,29 +50,36 @@ import Network.Socket
4550import Network.Wai (Application)
4651import qualified Network.Wai as W
4752import Network.Wai.Handler.Warp
53+import Network.Wai.Handler.WarpTLS
4854import Network.Wai.Handler.Warp.Internal (settingsHost, settingsPort)
4955import Network.WebSockets (ConnectionOptions)
5056import Network.WebSockets.Connection (defaultConnectionOptions)
5157import qualified Obelisk.Asset.Serve.Snap as Snap
52−import Obelisk.ExecutableConfig (get)
53−import Obelisk.ExecutableConfig.Inject (injectExecutableConfigs)
58+import Obelisk.Backend
5459import Obelisk.Frontend
5560import 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
5665import Reflex.Dom.Core
5766import Snap.Core (Snap)
5867import System.Environment
5968import System.IO
6069import System.Process
61−import System.Exit (ExitCode(..))
6270import Text.URI (URI)
6371import qualified Text.URI as URI
6472import Text.URI.Lens
65−import Obelisk.Backend
73+import Web.Cookie
74+
75+#if defined(IPROUTE_SUPPORTED)
76+import qualified System.Which
77+#endif
6678
6779run
6880 :: Int -- ^ Port to run the backend
6981 -> ([Text] -> Snap ()) -- ^ Static asset handler
70− -> Backend fullRoute frontendRoute -- ^ Backend
82+ -> Backend backendRoute frontendRoute -- ^ Backend
7183 -> Frontend (R frontendRoute) -- ^ Frontend
7284 -> IO ()
7385run port serveStaticAsset backend frontend = do
@@ -77,82 +89,126 @@ run port serveStaticAsset backend frontend = do
7789 case checkEncoder $ _backend_routeEncoder backend of
7890 Left e -> hPutStrLn stderr $ "backend error:\n" <> T.unpack e
7991 Right validFullEncoder -> do
80− backendTid <- forkIO $ handle handleBackendErr $ withArgs ["--quiet", "--port", show port] $ do
81− _backend_run backend $ \serveRoute -> do
82− runSnapWithCommandLineArgs $ do
92+ publicConfigs <- getPublicConfigs
93+ backendTid <- forkIO $ handle handleBackendErr $ withArgs ["--quiet", "--port", show port] $
94+ _backend_run backend $ \serveRoute ->
95+ runSnapWithCommandLineArgs $
8396 getRouteWith validFullEncoder >>= \case
8497 Identity r -> case r of
85− InL backendRoute :=> Identity a -> serveRoute $ backendRoute :/ a
86− InR obeliskRoute :=> Identity a ->
87− serveDefaultObeliskApp (mkRouteToUrl validFullEncoder) serveStaticAsset frontend $ obeliskRoute :/ a
98+ FullRoute_Backend backendRoute :/ a -> serveRoute $ backendRoute :/ a
99+ FullRoute_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+
88105 let conf = defRunConfig { _runConfig_redirectPort = port }
89− runWidget conf frontend validFullEncoder `finally` killThread backendTid
106+ runWidget conf publicConfigs frontend validFullEncoder `finally` killThread backendTid
90107
91108-- Convenience wrapper to handle path segments for 'Snap.serveAsset'
92109runServeAsset :: FilePath -> [Text] -> Snap ()
93110runServeAsset rootPath = Snap.serveAsset "" rootPath . T.unpack . T.intercalate "/"
94111
95−getConfigRoute :: IO (Maybe URI)
96−getConfigRoute = get "config/common/route" >>= \case
97− Just r -> case URI.mkURI $ T.strip r of
98− Just route -> pure $ Just route
99− Nothing -> do
100− putStrLn $ "Route is invalid: " <> show r
101− pure Nothing
102− Nothing -> pure Nothing
103−
104−defAppUri :: URI
105−defAppUri = fromMaybe (error "defAppUri") $ URI.mkURI "http://127.0.0.1:8000"
106−
107−runWidget :: RunConfig -> Frontend (R route) -> Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName -> IO ()
108−runWidget conf frontend validFullEncoder = do
109− uri <- fromMaybe defAppUri <$> getConfigRoute
112+getConfigRoute :: Map Text ByteString -> Either Text URI
113+getConfigRoute configs = case Map.lookup "common/route" configs of
114+ Just r ->
115+ let stripped = T.strip (T.decodeUtf8 r)
116+ in case URI.mkURI stripped of
117+ Just route -> Right route
118+ Nothing -> Left $ "Couldn't parse route as URI; value read was: " <> T.pack (show stripped)
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)
120+
121+runWidget
122+ :: RunConfig
123+ -> Map Text ByteString
124+ -> Frontend (R frontendRoute)
125+ -> Encoder Identity Identity (R (FullRoute backendRoute frontendRoute)) PageName
126+ -> IO ()
127+runWidget conf configs frontend validFullEncoder = do
128+ uri <- either (fail . T.unpack) pure $ getConfigRoute configs
110129 let port = fromIntegral $ fromMaybe 80 $ uri ^? uriAuthority . _Right . authPort . _Just
111130 redirectHost = _runConfig_redirectHost conf
112131 redirectPort = _runConfig_redirectPort conf
113132 beforeMainLoop = do
114133 putStrLn $ "Frontend running on " <> T.unpack (URI.render uri)
115134 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
116158 bracket
117159 (bindPortTCPRetry settings (logPortBindErr port) (_runConfig_retryTimeout conf))
118160 close
119161 (\skt -> do
120162 man <- newManager defaultManagerSettings
121− app <- obeliskApp defaultConnectionOptions frontend validFullEncoder uri $ fallbackProxy redirectHost redirectPort man
122− runSettingsSocket settings skt app)
163+ app <- obeliskApp configs defaultConnectionOptions frontend validFullEncoder uri $ fallbackProxy redirectHost redirectPort man
164+ runner settings skt app)
123165
124166obeliskApp
125− :: forall route backendRoute. ConnectionOptions
126− -> Frontend (R route)
127− -> Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName
167+ :: forall frontendRoute backendRoute
168+ . Map Text ByteString
169+ -> ConnectionOptions
170+ -> Frontend (R frontendRoute)
171+ -> Encoder Identity Identity (R (FullRoute backendRoute frontendRoute)) PageName
128172 -> URI
129173 -> Application
130174 -> IO Application
131−obeliskApp opts frontend validFullEncoder uri backend = do
132− let entryPoint = do
133− runFrontend validFullEncoder frontend
175+obeliskApp configs opts frontend validFullEncoder uri backend = do
176+ let mode = FrontendMode
177+ { _frontendMode_hydrate = True
178+ , _frontendMode_adjustRoute = False
179+ }
180+ entryPoint = do
181+ runFrontendWithConfigsAndCurrentRoute mode configs validFullEncoder frontend
134182 syncPoint
135183 jsaddlePath <- URI.mkPathPiece "jsaddle"
136184 let jsaddleUri = BSLC.fromStrict $ URI.renderBs $ uri & uriPath %~ (<>[jsaddlePath])
137185 Right (jsaddleWarpRouteValidEncoder :: Encoder Identity (Either Text) (R JSaddleWarpRoute) PageName) <- return $ checkEncoder jsaddleWarpRouteEncoder
138186 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
140188 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
142190 JSaddleWarpRoute_JavaScript :/ () -> sendResponse $ W.responseLBS H.status200 [("Content-Type", "application/javascript")] $ jsaddleJs' (Just jsaddleUri) False
143191 _ -> flip jsaddle sendResponse $ req
144192 { W.pathInfo = fst $ encode jsaddleWarpRouteValidEncoder jsaddleRoute
145193 }
146− InR (ObeliskRoute_App appRouteComponent) :=> Identity appRouteRest -> do
147− html <- renderJsaddleFrontend (mkRouteToUrl validFullEncoder) (appRouteComponent :/ appRouteRest) frontend
194+ FullRoute_Frontend (ObeliskRoute_App appRouteComponent) :/ appRouteRest -> do
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
148198 sendResponse $ W.responseLBS H.status200 [("Content-Type", staticRenderContentType)] $ BSLC.fromStrict html
149199 _ -> backend req sendResponse
150200
151−renderJsaddleFrontend :: (route -> Text) -> route -> Frontend route -> IO ByteString
152−renderJsaddleFrontend urlEnc r f =
201+renderJsaddleFrontend
202+ :: Map Text ByteString
203+ -> Cookies
204+ -> (route -> Text)
205+ -> route
206+ -> Frontend route
207+ -> IO ByteString
208+renderJsaddleFrontend configs cookies urlEnc r f =
153209 let jsaddleScript = elAttr "script" ("src" =: "/jsaddle/jsaddle.js") blank
154210 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
156212
157213-- | like 'bindPortTCP' but reconnects on exception
158214bindPortTCPRetry :: Settings
@@ -169,17 +225,22 @@ logPortBindErr p e = getProcessIdForPort p >>= \case
169225 Nothing -> putStrLn $ "runWidget: " <> show e
170226 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."]
171227
228+ssPath :: Maybe String
229+ssPath =
230+#if defined(IPROUTE_SUPPORTED)
231+ Just $(System.Which.staticWhich "ss")
232+#else
233+ Nothing
234+#endif
235+
172236getProcessIdForPort :: Int -> IO (Maybe Int)
173−getProcessIdForPort port = do
174− -- First check if 'ss' is available
175− (c, _, _) <- readProcessWithExitCode "which" ["ss"] mempty
176− case c of
177− ExitSuccess -> do
178− xs <- lines <$> readProcess "ss" ["-lptn", "sport = " <> show port] mempty
179− case uncons xs of
180− Just (_, x:_) -> return $ A.maybeResult $ A.parse parseSsPid $ BSC.pack x
181− _ -> return Nothing
182− _ -> return Nothing
237+getProcessIdForPort port = case ssPath of
238+ Just ss -> do
239+ xs <- lines <$> readProcess ss ["-lptn", "sport = " <> show port] mempty
240+ case uncons xs of
241+ Just (_, x:_) -> return $ A.maybeResult $ A.parse parseSsPid $ BSC.pack x
242+ _ -> return Nothing
243+ _ -> return Nothing
183244
184245parseSsPid :: A.Parser Int
185246parseSsPid = do
lib/selftest/obelisk-selftest.cabal+5−3
@@ -11,15 +11,17 @@ library
1111 base
1212 , containers
1313 , directory
14+ , filepath
1415 , hspec
1516 , http-client
1617 , http-types
1718 , HUnit
18− , obelisk-cliapp
19+ , network
20+ , obelisk-executable-config-lookup
1921 , obelisk-run
22+ , which
2023 , process
21− , network
22− , shelly
24+ , shelly >= 1.9.0
2325 , temporary
2426 , text
2527 exposed-modules:
lib/selftest/src/Obelisk/SelfTest.hs+325−191
@@ -1,39 +1,43 @@
1+{-# LANGUAGE DoAndIfThenElse #-}
12{-# LANGUAGE EmptyCase #-}
23{-# LANGUAGE LambdaCase #-}
34{-# LANGUAGE OverloadedStrings #-}
45{-# LANGUAGE PatternGuards #-}
56{-# LANGUAGE ScopedTypeVariables #-}
7+{-# LANGUAGE TemplateHaskell #-}
68{-# LANGUAGE TypeApplications #-}
9+{-# LANGUAGE ViewPatterns #-}
710module Obelisk.SelfTest where
811
9−import Control.Exception (bracket, throw)
12+import Control.Concurrent (threadDelay)
13+import Control.Exception (bracket, throw, try)
1014import Control.Monad
1115import Control.Monad.IO.Class
1216import Data.Bool (bool)
1317import Data.Function (fix)
14−import Data.Semigroup (Semigroup, (<>))
18+import Data.Semigroup ((<>))
1519import qualified Data.Set as Set
1620import Data.String
1721import Data.Text (Text)
18−import Data.Void
1922import qualified Data.Text as T
2023import qualified Data.Text.IO as T
2124import qualified Network.HTTP.Client as HTTP
2225import qualified Network.HTTP.Types as HTTP
2326import qualified Network.Socket as Socket
2427import Shelly
25−import System.Directory (withCurrentDirectory, getDirectoryContents)
28+import System.Directory (getCurrentDirectory, withCurrentDirectory, getDirectoryContents)
2629import System.Environment
2730import System.Exit (ExitCode (..))
28−import System.Info
29−import System.IO (Handle, hClose)
31+import System.FilePath (addTrailingPathSeparator)
32+import qualified System.Info
33+import System.IO (Handle, hClose, hIsEOF, hGetContents)
3034import System.IO.Temp
31−import System.Process (readProcessWithExitCode, CreateProcess(cwd), readCreateProcessWithExitCode, proc)
35+import System.Process (readProcessWithExitCode)
36+import System.Which (staticWhich)
3237import Test.Hspec
3338import Test.HUnit.Base
3439
35−import Obelisk.CliApp hiding (runCli, readCreateProcessWithExitCode)
36−import qualified Obelisk.CliApp as CliApp
40+import Obelisk.ExecutableConfig.Lookup (getConfigs)
3741import Obelisk.Run (getConfigRoute)
3842
3943data ObRunState
@@ -42,24 +46,47 @@ data ObRunState
4246 | ObRunState_BackendStarted
4347 deriving (Eq, Show)
4448
45−doubleQuotes :: (IsString a, Semigroup a) => a -> a
46−doubleQuotes s = "\"" <> s <> "\""
49+cpPath :: FilePath
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"]
4781
4882commit :: Text -> Sh ()
49−commit msg = void $ run "git"
50− [ "commit"
83+commit msg = void $ run gitPath $ gitUserConfig <> [ "commit"
5184 , "--no-gpg-sign"
5285 , "--allow-empty"
5386 , "-m"
54− , doubleQuotes msg
87+ , msg
5588 ]
5689
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−
6390tshow :: Show a => a -> Text
6491tshow = T.pack . show
6592
@@ -67,194 +94,294 @@ tshow = T.pack . show
6794shellyOb :: MonadIO m => (Sh a -> Sh a) -> Sh a -> m a
6895shellyOb f obTest = shelly $ f obTest
6996
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+
70129main :: IO ()
71130main = do
72131 -- Note: you can pass hspec arguments as well, eg: `-m <pattern>`
73− isVerbose <- (elem "-v") <$> getArgs
132+ isVerbose <- elem "-v" <$> getArgs
74133 unless isVerbose $
75134 putStrLn "Tests may take longer to run if there are unbuilt derivations: use -v for verbose output"
76− let verbosity = bool silently verbosely isVerbose
77− obeliskImpl <- fromString <$> getEnv "OBELISK_IMPL"
135+
136+ obeliskImplDirtyReadOnly <- getCurrentDirectory
78137 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
231138
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)
232358
233359-- | Run `ob run` in the given directory (maximum of one level deep)
234−testObRunInDir :: Socket.PortNumber -> Socket.PortNumber -> Maybe Shelly.FilePath -> HTTP.Manager -> Sh ()
235−testObRunInDir p0 p1 mdir httpManager = handle_sh (\case ExitSuccess -> pure (); e -> throw e) $ do
360+testObRunInDir :: String -> [Text] -> Maybe FilePath -> HTTP.Manager -> Sh ()
361+testObRunInDir executable extraArgs mdir httpManager = maskExitSuccess $ do
362+ [p0, p1] <- liftIO $ getFreePorts 2
236363 let uri p = "http://localhost:" <> T.pack (show p) <> "/" -- trailing slash required for comparison
237364 writefile "config/common/route" $ uri p0
238− maybe id chdir mdir $ runHandle "ob" ["run"] $ \stdout -> do
239− firstUri <- handleObRunStdout httpManager stdout
365+ maybe id chdir mdir $ runHandles executable extraArgs [] $ \_stdin stdout stderr -> do
366+ firstUri <- handleObRunStdout httpManager stdout stderr
240367 let newUri = uri p1
241368 when (firstUri == newUri) $ errorExit $
242369 "Startup URI (" <> firstUri <> ") is the same as test URI (" <> newUri <> ")"
243370 maybe id (\_ -> chdir "..") mdir $ alterRouteTo newUri stdout
244− runningUri <- handleObRunStdout httpManager stdout
371+ runningUri <- handleObRunStdout httpManager stdout stderr
245372 if runningUri /= newUri
246373 then errorExit $ "Reloading failed: expected " <> newUri <> " but got " <> runningUri
247374 else exit 0
248375
249−testThunkPack :: Shelly.FilePath -> Sh ()
250−testThunkPack path' = withTempFile (T.unpack $ toTextIgnore path') "test-file" $ \file handle -> do
251− let pack' = readProcessWithExitCode "ob" ["thunk", "pack", T.unpack $ toTextIgnore path'] ""
376+testThunkPack :: String -> [Text] -> FilePath -> Sh ()
377+testThunkPack executable args path' = withTempFile (T.unpack $ toTextIgnore path') "test-file" $ \file handle -> do
378+ let pack' = readProcessWithExitCode executable (T.unpack <$> ["thunk", "pack", toTextIgnore path'] ++ args) ""
252379 ensureThunkPackFails q = liftIO $ pack' >>= \case
253380 (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 <> "'"
255382 | q `T.isInfixOf` T.pack (out <> err) -> pure ()
256− | otherwise -> fail $ "ob thunk pack failed for an unexpected reason: " <> show out <> "\nstderr: " <> err
257− git = chdir path' . run "git"
383+ | otherwise -> fail $ "ob thunk pack failed for an unexpected reason, expecting '" <> show q <> "', received: " <> show out <> "\nstderr: " <> err
384+ git = chdir path' . run gitPath
258385 -- Untracked files
259386 ensureThunkPackFails "Untracked files"
260387 void $ git ["add", T.pack file]
@@ -267,7 +394,7 @@ testThunkPack path' = withTempFile (T.unpack $ toTextIgnore path') "test-file" $
267394 liftIO $ T.hPutStrLn handle "test file" >> hClose handle
268395 ensureThunkPackFails "modified"
269396 -- Existing stashes
270− void $ git ["stash"]
397+ void $ git $ gitUserConfig <> [ "stash" ]
271398 ensureThunkPackFails "has stashes"
272399
273400-- | Blocks until a non-empty line is available
@@ -288,9 +415,12 @@ alterRouteTo uri stdout = do
288415 "Reloading failed: " <> T.pack (show t)
289416
290417-- | Handle stdout of `ob run`: check that the frontend and backend servers are started correctly
291−handleObRunStdout :: HTTP.Manager -> Handle -> Sh Text
292−handleObRunStdout httpManager stdout = flip fix (ObRunState_Init, []) $ \loop (state, msgs) -> do
293− liftIO (T.hGetLine stdout) >>= \t -> case state of
418+handleObRunStdout :: HTTP.Manager -> Handle -> Handle -> Sh Text
419+handleObRunStdout httpManager stdout stderr = flip fix (ObRunState_Init, []) $ \loop (state, msgs) -> do
420+ isEOF <- liftIO $ hIsEOF stdout
421+ if isEOF
422+ then handleObRunError msgs
423+ else liftIO (T.hGetLine stdout) >>= \t -> case state of
294424 ObRunState_Init
295425 | "Running test..." `T.isPrefixOf` t -> loop (ObRunState_BackendStarted, msgs)
296426 ObRunState_Startup
@@ -302,8 +432,12 @@ handleObRunStdout httpManager stdout = flip fix (ObRunState_Init, []) $ \loop (s
302432 obRunCheck httpManager stdout uri
303433 pure uri
304434 | 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)
306436 | 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
307441
308442-- | Make requests to frontend/backend servers to check they are working properly
309443obRunCheck :: HTTP.Manager -> Handle -> Text -> Sh ()
lib/snap-extras/obelisk-snap-extras.cabal+1−1
@@ -1,6 +1,6 @@
11name: obelisk-snap-extras
22version: 0.1
3−synopsys: Extra functionality for Snap that should be considered for upstreaming
3+synopsis: Extra functionality for Snap that should be considered for upstreaming
44license: BSD3
55build-type: Simple
66cabal-version: >=1.2
lib/snap-extras/src/Obelisk/Snap/Extras.hs+1−1
@@ -2,13 +2,13 @@
22module Obelisk.Snap.Extras
33 ( cachePermanently
44 , doNotCache
5+ , ensureSecure
56 , serveFileIfExists
67 , serveFileIfExistsAs
78 ) where
89
910import Control.Monad.IO.Class
1011import Data.ByteString (ByteString)
11−import Data.Monoid
1212import Data.String
1313import Snap.Core
1414import Snap.Util.FileServe
lib/tabulation/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 @@
11self: super:
22
3−{
4− cleanHaskellSource = builtins.filterSource (name: _: let baseName = builtins.baseNameOf name; in !(
5− builtins.match "^\\.ghc\\.environment.*" baseName != null ||
6− baseName == "cabal.project.local"
7− ));
3+let
4+ inherit (self) lib;
5+ inherit (import ../dep/gitignore.nix { inherit lib; }) gitignoreSource;
6+in {
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);
813
9− obeliskExecutableConfig = self.callPackage ../lib/executable-config { };
14+ obeliskExecutableConfig = self.callPackage ../lib/executable-config {};
1015}
release.nix+8−78
@@ -1,80 +1,10 @@
1−{ local-self ? import ./. {}
1+{
2+ supportedSystems ? [ "x86_64-linux" "x86_64-darwin" ]
23}:
4+rec {
5+ recurseForDerivations = true;
6+ build = import ./all-builds.nix { inherit supportedSystems; };
7+ test = import ./all-tests.nix { inherit supportedSystems; };
38
4−let
5− inherit (local-self.nixpkgs) lib runCommand nix;
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; }
9+ inherit (build) metaCache;
10+}
scripts/ghcid-this-lib+1−1
@@ -1,4 +1,4 @@
11#!/usr/bin/env bash
22set -euo pipefail
33
4−nix-shell -E "with (import ../.. {}); reflex-platform.workOn haskellPackageSets.ghc haskellPackageSets.ghc.obelisk-$(basename $PWD)" --run "cabal configure ; ghcid -c'cabal repl'"
4+nix-shell -E "with (import ../.. {}); reflex-platform.workOn haskellPackageSets.ghc haskellPackageSets.ghc.obelisk-$(basename $PWD)" --run "ghcid -c 'cabal new-repl'"
skeleton/.gitignore+2
@@ -1,7 +1,9 @@
11dist-newstyle
22result
3+result-*
34result-android
45result-ios
56result-exe
67.attr-cache
78ghcid-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 #-}
61module Backend where
72
83import Common.Route
@@ -11,5 +6,5 @@ import Obelisk.Backend
116backend :: Backend BackendRoute FrontendRoute
127backend = Backend
138 { _backend_run = \serve -> serve $ const $ return ()
14− , _backend_routeEncoder = backendRouteEncoder
9+ , _backend_routeEncoder = fullRouteEncoder
1510 }
skeleton/common/common.cabal−1
@@ -9,7 +9,6 @@ library
99 , obelisk-route
1010 , mtl
1111 , text
12−
1312 exposed-modules:
1413 Common.Api
1514 Common.Route
skeleton/common/src/Common/Api.hs+1−1
@@ -1,4 +1,4 @@
11module Common.Api where
22
33commonStuff :: 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 @@
11{-# LANGUAGE EmptyCase #-}
22{-# LANGUAGE FlexibleContexts #-}
3+{-# LANGUAGE FlexibleInstances #-}
34{-# LANGUAGE GADTs #-}
4−{-# LANGUAGE LambdaCase #-}
5−{-# LANGUAGE RankNTypes #-}
6−{-# LANGUAGE TemplateHaskell #-}
75{-# LANGUAGE KindSignatures #-}
8−{-# LANGUAGE EmptyCase #-}
6+{-# LANGUAGE LambdaCase #-}
97{-# LANGUAGE MultiParamTypeClasses #-}
10−{-# LANGUAGE FlexibleInstances #-}
118{-# LANGUAGE OverloadedStrings #-}
12−
9+{-# LANGUAGE RankNTypes #-}
10+{-# LANGUAGE TemplateHaskell #-}
11+{-# LANGUAGE TypeFamilies #-}
1312module Common.Route where
1413
1514{- -- You will probably want these imports for composing Encoders.
@@ -19,7 +18,6 @@ import Control.Category
1918
2019import Data.Text (Text)
2120import Data.Functor.Identity
22−import Data.Functor.Sum
2321
2422import Obelisk.Route
2523import Obelisk.Route.TH
@@ -34,17 +32,14 @@ data FrontendRoute :: * -> * where
3432 FrontendRoute_Main :: FrontendRoute ()
3533 -- This type is used to define frontend routes, i.e. ones for which the backend will serve the frontend.
3634
37−backendRouteEncoder
38− :: Encoder (Either Text) Identity (R (Sum BackendRoute (ObeliskRoute FrontendRoute))) PageName
39−backendRouteEncoder = handleEncoder (const (InL BackendRoute_Missing :/ ())) $
40− pathComponentEncoder $ \case
41− InL backendRoute -> case backendRoute of
42− BackendRoute_Missing -> PathSegment "missing" $ unitEncoder mempty
43− InR obeliskRoute -> obeliskRouteSegment obeliskRoute $ \case
44− -- The encoder given to PathEnd determines how to parse query parameters,
45− -- in this example, we have none, so we insist on it.
46− FrontendRoute_Main -> PathEnd $ unitEncoder mempty
47−
35+fullRouteEncoder
36+ :: Encoder (Either Text) Identity (R (FullRoute BackendRoute FrontendRoute)) PageName
37+fullRouteEncoder = mkFullRouteEncoder
38+ (FullRoute_Backend BackendRoute_Missing :/ ())
39+ (\case
40+ BackendRoute_Missing -> PathSegment "missing" $ unitEncoder mempty)
41+ (\case
42+ FrontendRoute_Main -> PathEnd $ unitEncoder mempty)
4843
4944concat <$> mapM deriveRouteComponent
5045 [ ''BackendRoute
skeleton/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
2−, iosSdkVersion ? "10.2"
1+{ obelisk ? import ./.obelisk/impl {
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+ }
39}:
4−with import ./.obelisk/impl { inherit system iosSdkVersion; };
10+with obelisk;
511project ./. ({ ... }: {
612 android.applicationId = "systems.obsidian.obelisk.examples.minimal";
713 android.displayName = "Obelisk Minimal Example";
skeleton/frontend/frontend.cabal+2
@@ -9,7 +9,9 @@ library
99 , common
1010 , obelisk-frontend
1111 , obelisk-route
12+ , jsaddle
1213 , reflex-dom
14+ , obelisk-executable-config-lookup
1315 , obelisk-generated-static
1416 , text
1517 exposed-modules:
skeleton/frontend/src-bin/main.hs+1−1
@@ -6,5 +6,5 @@ import Reflex.Dom
66
77main :: IO ()
88main = do
9− let Right validFullEncoder = checkEncoder backendRouteEncoder
9+ let Right validFullEncoder = checkEncoder fullRouteEncoder
1010 run $ runFrontend validFullEncoder frontend
skeleton/frontend/src/Frontend.hs+28−3
@@ -1,23 +1,48 @@
11{-# LANGUAGE DataKinds #-}
22{-# LANGUAGE OverloadedStrings #-}
33{-# LANGUAGE TypeApplications #-}
4+
45module Frontend where
56
7+import Control.Monad
68import qualified Data.Text as T
9+import qualified Data.Text.Encoding as T
10+import Language.Javascript.JSaddle (eval, liftJSM)
11+
712import Obelisk.Frontend
13+import Obelisk.Configs
814import Obelisk.Route
15+import Obelisk.Generated.Static
16+
917import Reflex.Dom.Core
1018
1119import Common.Api
1220import Common.Route
13−import Obelisk.Generated.Static
1421
1522
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.
1626frontend :: Frontend (R FrontendRoute)
1727frontend = 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
1931 , _frontend_body = do
20− text "Welcome to Obelisk!"
32+ el "h1" $ text "Welcome to Obelisk!"
2133 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+
2241 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 ()
2348 }
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+}