on master help sign in
Merge remote-tracking branch 'origin/develop' into add-obelisk-profiling-feature
commit076047ff534810fe66fd135dc9cb6d4eb3dec8a1 authorElliot Cameron <eacameron@gmail.com> dateFri 13 Mar 2020, 17:58 (7 years ago) parentcf06275d8fd799
15 files changed+274−127
ChangeLog.md+8 README.md+41−19 default.nix+25−14 dep/gitignore.nix/default.nix+8 dep/gitignore.nix/github.json+8 dep/reflex-platform/github.json+3−3 dep/snap-core/default.nix+8 dep/snap-core/github.json+8 lib/asset/serve-snap/src/Obelisk/Asset/Serve/Snap.hs+14−4 lib/backend/src/Obelisk/Backend.hs+78−18 lib/command/src/Obelisk/Command.hs+2−2 lib/command/src/Obelisk/Command/Deploy.hs+61−60 lib/command/src/Obelisk/Command/Utils.hs+3 lib/frontend/src/Obelisk/Frontend.hs+6−6 lib/run/src/Obelisk/Run.hs+1−1
ChangeLog.md+8
@@ -2,6 +2,14 @@
22
33This project's release branch is `master`. This log is written from the perspective of the release branch: when changes hit `master`, they are considered released, and the date should reflect that release.
44
5+## Unreleased
6+
7+* 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))
8+* Support non-EC2 deployments. ([#658](https://github.com/obsidiansystems/obelisk/pull/658))
9+* Fix `ob deploy test android` to work. ([#645](https://github.com/obsidiansystems/obelisk/pull/645))
10+* 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.)
11+* Add `Obelisk.Backend.runBackendWith` to allow customization of how GHCJS resources are loaded in the page. ([#668](https://github.com/obsidiansystems/obelisk/pull/668))
12+
513## v0.6.0.0 - 2020-02-21
614
715* 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))
README.md+41−19
@@ -13,7 +13,9 @@ Obelisk provides an easy way to develop and deploy your [Reflex](https://github.
1313 - [Running over https](#running-over-https)
1414- [Deploying](#deploying)
1515 - [Locally](#locally)
16− - [EC2](#ec2)
16+ - [Default EC2 Deployment](#default-ec2-deployment)
17+ - [Custom Non-EC2 Deployment](#custom-non-ec2-deployment)
18+ - [VirtualBox Deployment](#virtualbox-deployment)
1719 - [From macOS](#from-macos)
1820 - [Deploying an updated version](#deploying-an-updated-version)
1921- [Mobile](#mobile)
@@ -55,24 +57,24 @@ Obelisk provides an easy way to develop and deploy your [Reflex](https://github.
5557
5658When developing on obelisk itself you may launch `ghcid` for the corresponding project as follows. For example to launch ghcid for `lib/backend` project:
5759
58−```
60+```bash
5961nix-shell -A obeliskEnvs.obelisk-backend --run "cd lib/backend && ghcid -c 'cabal new-repl'"
6062```
6163
6264Or to launch ghcid for `lib/command` project:
6365
64−```
66+```bash
6567nix-shell -A obeliskEnvs.obelisk-command --run "cd lib/command && ghcid -c 'cabal new-repl'"
6668```
6769
6870To re-install `ob` from source do
69−```
71+```bash
7072nix-env -f /path/to/obelisk -iA command
7173```
7274
7375Note that `ob` will defer to the version found in your project's `.obelisk/impl` directory. To update that version specifically:
7476
75−```shell
77+```bash
7678ob thunk unpack ./.obelisk/impl
7779cd ./.obelisk/impl
7880# apply your changes
@@ -80,7 +82,7 @@ cd ./.obelisk/impl
8082
8183If you want to commit your changes, first push them to your fork of obelisk and then
8284
83−```shell
85+```bash
8486cd /your/project/root
8587ob thunk pack .obelisk/impl
8688git add .obelisk/impl
@@ -100,13 +102,13 @@ access to:
100102
101103To create a new Obelisk project, go to an empty directory and run:
102104
103−```
105+```bash
104106ob init
105107```
106108
107109Obelisk 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:
108110
109−```
111+```bash
110112ob run
111113```
112114
@@ -165,7 +167,7 @@ project ./. ({ pkgs, ... }: {
165167# ...
166168```
167169
168−### Running over https
170+### Running over HTTPS
169171
170172To run your app locally over https, update the protocol in `config/common/route` to `https`, and then use `ob run` as normal.
171173
@@ -177,22 +179,22 @@ Since Obelisk generates a self-signed certificate for running https, the browser
177179
178180Build everything:
179181
180−```
182+```bash
181183nix-build -A exe --no-out-link
182184```
183185
184186Copy the result to a new directory, add configuration, and run!
185187
186−```
188+```bash
187189mkdir test-app
188190ln -s $(nix-build -A exe --no-out-link)/* test-app/
189191cp -r config test-app
190192(cd test-app && ./backend)
191193```
192194
193−### EC2
195+### Default EC2 Deployment
194196
195−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)).
196198
197199First create a new EC2 instance:
198200
@@ -219,7 +221,7 @@ ob deploy init \
219221 ~/code/myapp-deploy
220222```
221223
222−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.
223225
224226This 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.
225227
@@ -234,6 +236,26 @@ ob deploy push
234236
235237At 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`.
236238
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+
237259### From macOS
238260
239261Deploying from macOS requires some extra setup:
@@ -247,7 +269,7 @@ Running `ob deploy push` will give you additional setup instructions.
247269
248270If 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:
249271
250−```
272+```bash
251273cd ~/code/myapp-deploy
252274ob deploy update
253275ob deploy push
@@ -273,14 +295,14 @@ These versions will work out of the box but iOS SDKs prior to 11.3 should also w
273295
274296More recent Xcodes should also work, as long as one of the SDKs mentioned above has been used.
275297To 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.
276−```
298+```bash
277299open -W Xcode_9.2.xip
278300sudo 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
279301```
280302
281303
282304You can verify that you have correct versions by running
283−```
305+```bash
284306xcodebuild -showsdks
285307```
286308
@@ -306,7 +328,7 @@ Ensure that `bundleIdentifier` matches the App ID of the development profile, or
3063281. Connect the registered iPhone.
3073291. Find your Apple Team ID in the [developer portal](https://developer.apple.com/account/#/membership).
3083301. Run the deploy command with your Team ID:
309−```
331+```bash
310332result-ios/bin/deploy [TEAM_ID]
311333# or in debug mode via lldb:
312334result-ios/bin/deploy [TEAM_ID] -d
@@ -316,7 +338,7 @@ result-ios/bin/deploy [TEAM_ID] -d
3163381. Go to [developer portal - distribution profiles](https://developer.apple.com/account/ios/profile/production).
317339Create and download a distribution profile.
3183401. Run the package script with your TEAM ID and your distribution profile to create a `.ipa`:
319−```
341+```bash
320342result-ios/bin/package [TEAM_ID] /path/to/output/.ipa /path/to/profile/file
321343```
322344
default.nix+25−14
@@ -5,10 +5,18 @@
55, reflex-platform-func ? import ./dep/reflex-platform
66}:
77let
8− cleanSource = builtins.filterSource (name: _: let baseName = builtins.baseNameOf name; in !(
9− builtins.match "^\\.ghc\\.environment.*" baseName != null ||
10− baseName == "cabal.project.local"
11− ));
8+ reflex-platform = getReflexPlatform { inherit system; };
9+ inherit (reflex-platform) hackGet nixpkgs;
10+ pkgs = nixpkgs;
11+
12+ inherit (import dep/gitignore.nix { inherit (nixpkgs) lib; }) gitignoreSource;
13+
14+ cleanSource = src:
15+ # WARNING: The order of application here seems to matter a great deal to
16+ # how quickly `ghcid` is able to reload changes. As a rule of thumb,
17+ # always apply `gitignoreSource` first.
18+ # See https://github.com/obsidiansystems/obelisk/pull/666 and related.
19+ pkgs.lib.cleanSource (gitignoreSource src);
1220
1321 commandRuntimeDeps = pkgs: with pkgs; [
1422 coreutils
@@ -37,6 +45,8 @@ let
3745 hnix-store-core = self.callHackage "hnix-store-core" "0.1.0.0" {};
3846
3947 ghcid = self.callCabal2nix "ghcid" (hackGet ./dep/ghcid) {};
48+ # Exports more internals
49+ snap-core = haskellLib.dontCheck (self.callCabal2nix "snap-core" (hackGet ./dep/snap-core) {});
4050 })
4151
4252 pkgs.obeliskExecutableConfig.haskellOverlay
@@ -56,6 +66,7 @@ let
5666 obelisk-cliapp = self.callCabal2nix "obelisk-cliapp" (cleanSource ./lib/cliapp) {};
5767 obelisk-command = haskellLib.overrideCabal (self.callCabal2nix "obelisk-command" (cleanSource ./lib/command) {}) {
5868 librarySystemDepends = [
69+ pkgs.jre
5970 pkgs.nix
6071 (haskellLib.justStaticExecutables self.ghcid)
6172 ];
@@ -100,10 +111,6 @@ let
100111 ];
101112 };
102113
103− reflex-platform = getReflexPlatform { inherit system; };
104− inherit (reflex-platform) hackGet nixpkgs;
105− pkgs = nixpkgs;
106−
107114 # The haskell environment used to build Obelisk itself, e.g. the 'ob' command
108115 ghcObelisk = reflex-platform.ghc;
109116
@@ -165,10 +172,14 @@ in rec {
165172 '';
166173
167174 serverModules = {
168− mkBaseEc2 = { hostName, routeHost, enableHttps, adminEmail, ... }: {...}: {
175+ mkBaseEc2 = { nixosPkgs, ... }: {...}: {
169176 imports = [
170− (pkgs.path + /nixos/modules/virtualisation/amazon-image.nix)
177+ (nixosPkgs.path + /nixos/modules/virtualisation/amazon-image.nix)
171178 ];
179+ ec2.hvm = true;
180+ };
181+
182+ mkDefaultNetworking = { adminEmail, enableHttps, hostName, routeHost, ... }: {...}: {
172183 networking = {
173184 inherit hostName;
174185 firewall.allowedTCPPorts = if enableHttps then [ 80 443 ] else [ 80 ];
@@ -176,7 +187,6 @@ in rec {
176187 security.acme.certs = if enableHttps then {
177188 "${routeHost}".email = adminEmail;
178189 } else {};
179− ec2.hvm = true;
180190 };
181191
182192 mkObeliskApp =
@@ -243,14 +253,15 @@ in rec {
243253 echo ${version} > $out/version
244254 '';
245255
246− server = { exe, hostName, adminEmail, routeHost, enableHttps, version }@args:
256+ server = { exe, hostName, adminEmail, routeHost, enableHttps, version, module ? serverModules.mkBaseEc2 }@args:
247257 let
248258 nixos = import (pkgs.path + /nixos);
249259 in nixos {
250260 system = "x86_64-linux";
251261 configuration = {
252262 imports = [
253− (serverModules.mkBaseEc2 args)
263+ (module { inherit exe hostName adminEmail routeHost enableHttps version; nixosPkgs = pkgs; })
264+ (serverModules.mkDefaultNetworking args)
254265 (serverModules.mkObeliskApp args)
255266 ];
256267 };
@@ -406,7 +417,7 @@ in rec {
406417 linuxExeConfigurable = linuxExe;
407418 linuxExe = linuxExe dummyVersion;
408419 exe = serverOn mainProjectOut dummyVersion;
409− server = args@{ hostName, adminEmail, routeHost, enableHttps, version }:
420+ server = args@{ hostName, adminEmail, routeHost, enableHttps, version, module ? serverModules.mkBaseEc2 }:
410421 server (args // { exe = linuxExe version; });
411422 obelisk = import (base' + "/.obelisk/impl") {};
412423 };
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/reflex-platform/github.json+3−3
@@ -1,8 +1,8 @@
11{
22 "owner": "reflex-frp",
33 "repo": "reflex-platform",
4− "branch": "bump-reflex-0-6-4-1",
4+ "branch": "develop",
55 "private": false,
6− "rev": "38060894cb377160d1217305334f7f11202042d4",
7− "sha256": "0n47fcwj9szy3aqip0smmxdsp9fr78985rdslpmx2zdlkswmm5r1"
6+ "rev": "156ebb7c391ec909d19e794e591f107462862543",
7+ "sha256": "0rvbaimcvcica80538i6vxckv8si1gbd1ynvgxv7zr5jd1gk6vi5"
88}
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+}
lib/asset/serve-snap/src/Obelisk/Asset/Serve/Snap.hs+14−4
@@ -12,12 +12,13 @@ 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)
2223import Control.Monad.Fail (MonadFail)
2324import Data.Attoparsec.ByteString (parseOnly, endOfInput)
@@ -94,8 +95,17 @@ serveAsset' doRedirect base fallback p = do
9495 let finalFilename = base </> p </> "encodings" </> T.unpack (decodeUtf8 e)
9596 stat <- liftIO $ getFileStatus finalFilename
9697 modifyResponse $ setHeader "Last-Modified" "Thu, 1 Jan 1970 00:00:00 GMT"
97− modifyResponse $ setResponseCode 200 . setContentLength (fromIntegral $ fileSize stat) . setContentType (fileType defaultMimeTypes p)
98− 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
99109 Just _ -> cachePermanently >> modifyResponse (setResponseCode 304)
100110 Right "redirect" -> do
101111 mtarget <- liftIO $ getAssetTarget $ base </> p
lib/backend/src/Obelisk/Backend.hs+78−18
@@ -1,3 +1,4 @@
1+{-# LANGUAGE DeriveFunctor #-}
12{-# LANGUAGE FlexibleContexts #-}
23{-# LANGUAGE FlexibleInstances #-}
34{-# LANGUAGE GADTs #-}
@@ -9,6 +10,17 @@
910{-# OPTIONS_GHC -fno-warn-orphans #-}
1011module Obelisk.Backend
1112 ( Backend (..)
13+ -- * Running a backend
14+ , runBackend
15+ , runBackendWith
16+ -- * Configuration of backend
17+ , GhcjsWidgets(..)
18+ , defaultGhcjsWidgets
19+ -- * all.js script loading functions
20+ , deferredGhcjsScript
21+ , delayedGhcjsScript
22+ -- * all.js preload functions
23+ , preloadGhcjs
1224 -- * Re-exports
1325 , Default (def)
1426 , getPageName
@@ -17,7 +29,6 @@ module Obelisk.Backend
1729 , serveDefaultObeliskApp
1830 , prettifyOutput
1931 , renderAllJsPath
20− , runBackend
2132 , staticRenderContentType
2233 , getPublicConfigs
2334 ) where
@@ -58,19 +69,35 @@ data GhcjsApp route = GhcjsApp
5869 , _ghcjsApp_value :: !(Frontend route)
5970 }
6071
72+-- | Widgets used to load all.js on the frontend
73+data GhcjsWidgets a = GhcjsWidgets
74+ { _ghcjsWidgets_preload :: a
75+ -- ^ A preload widget, placed in the document head
76+ , _ghcjsWidgets_script :: a
77+ -- ^ A script widget, placed in the document body
78+ } deriving Functor
79+
80+-- | Given the URL of all.js, return the widgets which are responsible for
81+-- loading the script. Defaults to 'preloadGhcjs' and 'deferredGhcjsScript'.
82+defaultGhcjsWidgets :: GhcjsWidgets (Text -> FrontendWidgetT r ())
83+defaultGhcjsWidgets = GhcjsWidgets
84+ { _ghcjsWidgets_preload = preloadGhcjs
85+ , _ghcjsWidgets_script = deferredGhcjsScript
86+ }
87+
6188-- | Serve a frontend, which must be the same frontend that Obelisk has built and placed in the default location
6289--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
6390serveDefaultObeliskApp
6491 :: (MonadSnap m, HasCookies m, MonadFail m)
6592 => (R appRoute -> Text)
66− -> Text -- ^ URL to GHCJS app JavaScript
93+ -> GhcjsWidgets (FrontendWidgetT (R appRoute) ())
6794 -> ([Text] -> m ())
6895 -> Frontend (R appRoute)
6996 -> Map Text ByteString
7097 -> R (ObeliskRoute appRoute)
7198 -> m ()
72−serveDefaultObeliskApp urlEnc allJsUrl serveStaticAsset frontend =
73− serveObeliskApp urlEnc allJsUrl serveStaticAsset frontendApp
99+serveDefaultObeliskApp urlEnc ghcjsWidgets serveStaticAsset frontend =
100+ serveObeliskApp urlEnc ghcjsWidgets serveStaticAsset frontendApp
74101 where frontendApp = GhcjsApp
75102 { _ghcjsApp_compiled = defaultFrontendGhcjsAssets
76103 , _ghcjsApp_value = frontend
@@ -124,17 +151,17 @@ renderAllJsPath validFullEncoder =
124151serveObeliskApp
125152 :: (MonadSnap m, HasCookies m, MonadFail m)
126153 => (R appRoute -> Text)
127− -> Text -- ^ URL to GHCJS app JavaScript
154+ -> GhcjsWidgets (FrontendWidgetT (R appRoute) ())
128155 -> ([Text] -> m ())
129156 -> GhcjsApp (R appRoute)
130157 -> Map Text ByteString
131158 -> R (ObeliskRoute appRoute)
132159 -> m ()
133−serveObeliskApp urlEnc allJsUrl serveStaticAsset frontendApp config = \case
134− ObeliskRoute_App appRouteComponent :=> Identity appRouteRest -> serveGhcjsApp urlEnc allJsUrl frontendApp config $ GhcjsAppRoute_App appRouteComponent :/ appRouteRest
160+serveObeliskApp urlEnc ghcjsWidgets serveStaticAsset frontendApp config = \case
161+ ObeliskRoute_App appRouteComponent :=> Identity appRouteRest -> serveGhcjsApp urlEnc ghcjsWidgets frontendApp config $ GhcjsAppRoute_App appRouteComponent :/ appRouteRest
135162 ObeliskRoute_Resource resComponent :=> Identity resRest -> case resComponent :=> Identity resRest of
136163 ResourceRoute_Static :=> Identity pathSegments -> serveStaticAsset pathSegments
137− ResourceRoute_Ghcjs :=> Identity pathSegments -> serveGhcjsApp urlEnc allJsUrl frontendApp config $ GhcjsAppRoute_Resource :/ pathSegments
164+ ResourceRoute_Ghcjs :=> Identity pathSegments -> serveGhcjsApp urlEnc ghcjsWidgets frontendApp config $ GhcjsAppRoute_Resource :/ pathSegments
138165 ResourceRoute_JSaddleWarp :=> Identity _ -> do
139166 let msg = "Error: Obelisk.Backend received jsaddle request"
140167 liftIO $ putStrLn $ T.unpack msg
@@ -161,20 +188,29 @@ staticRenderContentType = "text/html; charset=utf-8"
161188serveGhcjsApp
162189 :: (MonadSnap m, HasCookies m, MonadFail m)
163190 => (R appRouteComponent -> Text)
164− -> Text -- ^ URL to GHCJS app JavaScript
191+ -> GhcjsWidgets (FrontendWidgetT (R appRouteComponent) ())
165192 -> GhcjsApp (R appRouteComponent)
166193 -> Map Text ByteString
167194 -> R (GhcjsAppRoute appRouteComponent)
168195 -> m ()
169−serveGhcjsApp urlEnc allJsUrl app config = \case
196+serveGhcjsApp urlEnc ghcjsWidgets app config = \case
170197 GhcjsAppRoute_App appRouteComponent :=> Identity appRouteRest -> do
171198 modifyResponse $ setContentType staticRenderContentType
172199 modifyResponse $ setHeader "Cache-Control" "no-store private"
173− writeBS <=< renderGhcjsFrontend urlEnc allJsUrl (appRouteComponent :/ appRouteRest) config $ _ghcjsApp_value app
200+ writeBS <=< renderGhcjsFrontend urlEnc ghcjsWidgets (appRouteComponent :/ appRouteRest) config $ _ghcjsApp_value app
174201 GhcjsAppRoute_Resource :=> Identity pathSegments -> serveStaticAssets (_ghcjsApp_compiled app) pathSegments
175202
176203runBackend :: Backend backendRoute frontendRoute -> Frontend (R frontendRoute) -> IO ()
177−runBackend backend frontend = case checkEncoder $ _backend_routeEncoder backend of
204+runBackend = runBackendWith defaultGhcjsWidgets
205+
206+runBackendWith
207+ :: GhcjsWidgets (Text -> FrontendWidgetT (R frontendRoute) ())
208+ -- ^ Given the URL of all.js, return the widgets which are responsible for
209+ -- loading the script.
210+ -> Backend backendRoute frontendRoute
211+ -> Frontend (R frontendRoute)
212+ -> IO ()
213+runBackendWith ghcjsWidgets backend frontend = case checkEncoder $ _backend_routeEncoder backend of
178214 Left e -> fail $ "backend error:\n" <> T.unpack e
179215 Right validFullEncoder -> do
180216 publicConfigs <- getPublicConfigs
@@ -184,7 +220,7 @@ runBackend backend frontend = case checkEncoder $ _backend_routeEncoder backend
184220 Identity r -> case r of
185221 FullRoute_Backend backendRoute :/ a -> serveRoute $ backendRoute :/ a
186222 FullRoute_Frontend obeliskRoute :/ a ->
187− serveDefaultObeliskApp routeToUrl allJsUrl (serveStaticAssets defaultStaticAssets) frontend publicConfigs $
223+ serveDefaultObeliskApp routeToUrl (($ allJsUrl) <$> ghcjsWidgets) (serveStaticAssets defaultStaticAssets) frontend publicConfigs $
188224 obeliskRoute :/ a
189225 where
190226 routeToUrl (k :/ v) = renderObeliskRoute validFullEncoder $ FullRoute_Frontend (ObeliskRoute_App k) :/ v
@@ -193,16 +229,40 @@ runBackend backend frontend = case checkEncoder $ _backend_routeEncoder backend
193229renderGhcjsFrontend
194230 :: (MonadSnap m, HasCookies m)
195231 => (route -> Text)
196− -> Text -- ^ URL to GHCJS app JavaScript
232+ -> GhcjsWidgets (FrontendWidgetT route ())
197233 -> route
198234 -> Map Text ByteString
199235 -> Frontend route
200236 -> m ByteString
201−renderGhcjsFrontend urlEnc allJsUrl route configs f = do
202− let ghcjsPreload = elAttr "link" ("rel" =: "preload" <> "as" =: "script" <> "href" =: allJsUrl) blank
203− ghcjsScript = elAttr "script" ("language" =: "javascript" <> "src" =: allJsUrl <> "defer" =: "defer") blank
237+renderGhcjsFrontend urlEnc ghcjsWidgets route configs f = do
204238 cookies <- askCookies
205− renderFrontendHtml configs cookies urlEnc route f ghcjsPreload ghcjsScript
239+ renderFrontendHtml configs cookies urlEnc route f (_ghcjsWidgets_preload ghcjsWidgets) (_ghcjsWidgets_script ghcjsWidgets)
240+
241+-- | Preload all.js in a link tag.
242+-- This is the default preload method.
243+preloadGhcjs :: Text -> FrontendWidgetT r ()
244+preloadGhcjs allJsUrl = elAttr "link" ("rel" =: "preload" <> "as" =: "script" <> "href" =: allJsUrl) blank
245+
246+-- | Load the script from the given URL in a deferred script tag.
247+-- This is the default method.
248+deferredGhcjsScript :: Text -> FrontendWidgetT r ()
249+deferredGhcjsScript allJsUrl = elAttr "script" ("type" =: "text/javascript" <> "src" =: allJsUrl <> "defer" =: "defer") blank
250+
251+-- | An all.js script which is loaded after waiting for some time to pass. This
252+-- is useful to ensure any CSS animations on the page can play smoothly before
253+-- blocking the UI thread by running all.js.
254+delayedGhcjsScript
255+ :: Int -- ^ The number of milliseconds to delay loading by
256+ -> Text -- ^ URL to GHCJS app JavaScript
257+ -> FrontendWidgetT r ()
258+delayedGhcjsScript n allJsUrl = elAttr "script" ("type" =: "text/javascript") $ text $ T.unlines
259+ [ "setTimeout(function() {"
260+ , " var all_js_script = document.createElement('script');"
261+ , " all_js_script.type = 'text/javascript';"
262+ , " all_js_script.src = '" <> allJsUrl <> "';"
263+ , " document.body.appendChild(all_js_script);"
264+ , "}, " <> T.pack (show n) <> ");"
265+ ]
206266
207267instance HasCookies Snap where
208268 askCookies = map (\c -> (cookieName c, cookieValue c)) <$> getsRequest rqCookies
lib/command/src/Obelisk/Command.hs+2−2
@@ -157,8 +157,8 @@ deployCommand cfg = hsubparser $ mconcat
157157
158158deployInitOpts :: Parser DeployInitOpts
159159deployInitOpts = DeployInitOpts
160− <$> strArgument (action "directory" <> metavar "DEPLOYDIR" <> help "Path to a directory that it will create")
161− <*> strOption (long "ssh-key" <> action "file" <> metavar "SSHKEY" <> help "Path to an ssh key that it will symlink to")
160+ <$> strArgument (action "directory" <> metavar "DEPLOYDIR" <> help "Path to a directory where the deployment repository will be initialized")
161+ <*> strOption (long "ssh-key" <> action "file" <> metavar "SSHKEY" <> help "Path to an SSH key that will be *copied* to the deployment repository")
162162 <*> some (strOption (long "hostname" <> metavar "HOSTNAME" <> help "hostname of the deployment target"))
163163 <*> strOption (long "route" <> metavar "PUBLICROUTE" <> help "Publicly accessible URL of your app")
164164 <*> strOption (long "admin-email" <> metavar "ADMINEMAIL" <> help "Email address where administrative alerts will be sent")
lib/command/src/Obelisk/Command/Deploy.hs+61−60
@@ -8,22 +8,17 @@ module Obelisk.Command.Deploy where
88
99import Control.Lens
1010import Control.Monad
11−import Control.Monad.Catch (Exception (displayException), MonadThrow, throwM, try, bracket_)
11+import Control.Monad.Catch (Exception (displayException), MonadThrow, bracket, throwM, try)
1212import Control.Monad.IO.Class (liftIO)
1313import Data.Aeson (FromJSON, ToJSON, encode, eitherDecode)
1414import Data.Bits
1515import qualified Data.ByteString.Lazy as BSL
1616import Data.Default
17−import qualified Data.HashMap.Strict as HM
1817import qualified Data.Map as Map
19−import Data.Text (Text)
2018import qualified Data.Set as Set
2119import qualified Data.Text as T
2220import qualified Data.Text.IO as T
2321import GHC.Generics
24−import Nix.Pretty
25−import Nix.String (principledMakeNixStringWithoutContext)
26−import Nix.Value
2722import System.Directory
2823import System.FilePath
2924import System.IO
@@ -53,6 +48,7 @@ deployInit
5348 -> Bool -- ^ enable https
5449 -> m ()
5550deployInit thunkPtr deployDir sshKeyPath hostnames route adminEmail enableHttps = do
51+ liftIO $ createDirectoryIfMissing True deployDir
5652 localKey <- withSpinner ("Preparing " <> T.pack deployDir) $ do
5753 localKey <- liftIO (doesFileExist sshKeyPath) >>= \case
5854 False -> failWith $ T.pack $ "ob deploy init: file does not exist: " <> sshKeyPath
@@ -79,22 +75,30 @@ deployInit thunkPtr deployDir sshKeyPath hostnames route adminEmail enableHttps
7975 , deployDir </> "config" </> "common"
8076 , deployDir </> "config" </> "frontend"
8177 ]
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+
8284 withSpinner "Writing deployment configuration" $ do
8385 writeDeployConfig deployDir "backend_hosts" $ unlines hostnames
8486 writeDeployConfig deployDir "enable_https" $ show enableHttps
8587 writeDeployConfig deployDir "admin_email" adminEmail
8688 writeDeployConfig deployDir ("config" </> "common" </> "route") route
87− withSpinner "Creating source thunk (./src)" $ liftIO $ do
88− createThunk (deployDir </> "src") thunkPtr
89− setupObeliskImpl deployDir
89+ writeDeployConfig deployDir "module.nix" $
90+ "(import " <> toNixPath (makeRelative deployDir srcDir) <> " {}).obelisk.serverModules.mkBaseEc2"
91+
9092 withSpinner ("Initializing git repository (" <> T.pack deployDir <> ")") $
9193 initGit deployDir
9294
9395setupObeliskImpl :: FilePath -> IO ()
9496setupObeliskImpl deployDir = do
95− let implDir = toImplDir deployDir
97+ let
98+ implDir = toImplDir deployDir
99+ goBackUp = foldr (</>) "" $ (".." <$) $ splitPath $ makeRelative deployDir implDir
96100 createDirectoryIfMissing True implDir
97− writeFile (implDir </> "default.nix") "(import ../../src {}).obelisk"
101+ writeFile (implDir </> "default.nix") $ "(import " <> toNixPath (goBackUp </> "src") <> " {}).obelisk"
98102
99103deployPush :: MonadObelisk m => FilePath -> m [String] -> m ()
100104deployPush deployPath getNixBuilders = do
@@ -113,6 +117,8 @@ deployPush deployPath getNixBuilders = do
113117 Left err -> failWith $ "ob deploy push: couldn't read src thunk: " <> T.pack (show err)
114118 let version = show . _thunkRev_commit $ _thunkPtr_rev thunkPtr
115119 builders <- getNixBuilders
120+ let moduleFile = deployPath </> "module.nix"
121+ moduleFileExists <- liftIO $ doesFileExist moduleFile
116122 buildOutputByHost <- ifor (Map.fromSet (const ()) hosts) $ \host () -> do
117123 --TODO: What does it mean if this returns more or less than 1 line of output?
118124 [result] <- fmap lines $ nixCmd $ NixCmd_Build $ def
@@ -122,13 +128,13 @@ deployPush deployPath getNixBuilders = do
122128 , _target_expr = Nothing
123129 }
124130 & nixBuildConfig_outLink .~ OutLink_None
125− & nixCmdConfig_args .~
131+ & nixCmdConfig_args .~ (
126132 [ strArg "hostName" host
127133 , strArg "adminEmail" adminEmail
128134 , strArg "routeHost" routeHost
129135 , strArg "version" version
130136 , boolArg "enableHttps" enableHttps
131− ]
137+ ] <> [rawArg "module" ("import " <> toNixPath moduleFile) | moduleFileExists ])
132138 & nixCmdConfig_builders .~ builders
133139 pure result
134140 let knownHostsPath = deployPath </> "backend_known_hosts"
@@ -172,20 +178,6 @@ deployPush deployPath getNixBuilders = do
172178deployUpdate :: MonadObelisk m => FilePath -> m ()
173179deployUpdate deployPath = updateThunkToLatest (ThunkUpdateConfig Nothing (ThunkConfig Nothing)) (deployPath </> "src")
174180
175−keytoolToAndroidConfig :: KeytoolConfig -> HM.HashMap Text (NValue t Identity m)
176−keytoolToAndroidConfig conf =
177− let path = nvPath $ _keytoolConfig_keystore conf
178− fromStr = nvStr . principledMakeNixStringWithoutContext . T.pack
179− storepass = fromStr $ _keytoolConfig_storepass conf
180− alias = fromStr $ _keytoolConfig_alias conf
181− keypass = fromStr $ _keytoolConfig_keypass conf
182− in HM.fromList
183− [ ("storeFile", path)
184− , ("storePassword", storepass)
185− , ("keyAlias", alias)
186− , ("keyPassword", keypass)
187− ]
188−
189181data PlatformDeployment = Android | IOS
190182 deriving (Show, Eq)
191183
@@ -200,7 +192,7 @@ deployMobile platform mobileArgs = withProjectRoot "." $ \root -> do
200192 configDir = root </> "config"
201193 exists <- liftIO $ doesDirectoryExist srcDir
202194 unless exists $ failWith "ob test should be run inside of a deploy directory"
203− nixBuildTarget <- case platform of
195+ (nixBuildTarget, extraArgs) <- case platform of
204196 Android -> do
205197 let keystorePath = root </> "android_keystore.jks"
206198 keytoolConfPath = root </> "android_keytool_config.json"
@@ -224,43 +216,55 @@ deployMobile platform mobileArgs = withProjectRoot "." $ \root -> do
224216 checkKeytoolConfExist <- liftIO $ doesFileExist keytoolConfPath
225217 unless checkKeytoolConfExist $ failWith "Missing android KeytoolConfig"
226218 keytoolConfContents <- liftIO $ BSL.readFile keytoolConfPath
227− liftIO $ print keytoolConfContents
228− releaseKey :: String <- case eitherDecode keytoolConfContents :: Either String KeytoolConfig of
219+ keyArgs <- case eitherDecode keytoolConfContents :: Either String KeytoolConfig of
229220 Left err -> failWith $ T.pack err
230− Right conf -> return $
231− (printNix :: MonadObelisk m => NValue t Identity m -> String) $
232− nvSet (keytoolToAndroidConfig conf) HM.empty
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+ ]
233228 let expr = mconcat
234− [ "with (import ", srcDir, " {});"
235− , "android.frontend.override (drv: { "
236− , "releaseKey = (if builtins.isNull drv.releaseKey then {} else drv.releaseKey) // " <> releaseKey <> "; "
229+ [ "with (import ", toNixPath srcDir, " {});"
230+ , "android.frontend.override (drv: {"
231+ , "isRelease = true;"
232+ , "staticSrc = (passthru.__androidWithConfig ", configDir, ").frontend.staticSrc;"
237233 , "assets = (passthru.__androidWithConfig ", configDir, ").frontend.assets;"
238234 , "})"
239235 ]
240− return $ Target
236+ return (Target
241237 { _target_path = Nothing
242238 , _target_attr = Nothing
243239 , _target_expr = Just expr
244− }
240+ }, keyArgs)
245241 IOS -> do
246242 let expr = mconcat
247− [ "with (import ", srcDir, " {});"
248− , "ios.frontend.override (_: { staticSrc = (passthru.__iosWithConfig ", configDir, ").frontend.staticSrc; })"
243+ [ "with (import ", toNixPath srcDir, " {});"
244+ , "ios.frontend.override (_: { staticSrc = (passthru.__iosWithConfig ", toNixPath configDir, ").frontend.staticSrc; })"
249245 ]
250− return $ Target
246+ return (Target
251247 { _target_path = Nothing
252248 , _target_attr = Nothing
253249 , _target_expr = Just expr
254− }
250+ }, [])
255251 result <- nixCmd $ NixCmd_Build $ def
256252 & nixBuildConfig_outLink .~ OutLink_None
257253 & nixCmdConfig_target .~ nixBuildTarget
258− putLog Notice $ T.pack $ "Your recently built android apk can be found at the following path: " <> show result
259− 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)
260259 where
261− withEcho showEcho f = do
262− prevEcho <- hGetEcho stdin
263− 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)
264268
265269data KeytoolConfig = KeytoolConfig
266270 { _keytoolConfig_keystore :: FilePath
@@ -273,19 +277,16 @@ instance FromJSON KeytoolConfig
273277instance ToJSON KeytoolConfig
274278
275279createKeystore :: MonadObelisk m => FilePath -> KeytoolConfig -> m ()
276−createKeystore root config = do
277− let expr = "with (import " <> toImplDir root <> ").reflex-platform.nixpkgs; pkgs.mkShell { buildInputs = [ pkgs.jdk ]; }"
278− callProcessAndLogOutput (Notice,Notice) $ setCwd (Just root) $ proc "nix-shell" ["default.nix", "-E" , expr, "--run" , keytoolCmd]
279− where
280− keytoolCmd = processToShellString "keytool"
281− [ "-genkeypair", "-noprompt"
282− , "-keystore", _keytoolConfig_keystore config
283− , "-keyalg", "RSA", "-keysize", "2048"
284− , "-validity", "1000000"
285− , "-storepass", _keytoolConfig_storepass config
286− , "-alias", _keytoolConfig_alias config
287− , "-keypass", _keytoolConfig_keypass config
288− ]
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+ ]
289290
290291-- | Simplified deployment configuration mechanism. At one point we may revisit this.
291292writeDeployConfig :: MonadObelisk m => FilePath -> FilePath -> String -> m ()
lib/command/src/Obelisk/Command/Utils.hs+3
@@ -44,6 +44,9 @@ nixExePath = $(staticWhich "nix")
4444nixBuildExePath :: FilePath
4545nixBuildExePath = $(staticWhich "nix-build")
4646
47+jreKeyToolPath :: FilePath
48+jreKeyToolPath = $(staticWhich "keytool")
49+
4750-- Check whether the working directory is clean
4851checkGitCleanStatus :: MonadObelisk m => FilePath -> Bool -> m Bool
4952checkGitCleanStatus repo withIgnored = do
lib/frontend/src/Obelisk/Frontend.hs+6−6
@@ -20,6 +20,7 @@ module Obelisk.Frontend
2020 , renderFrontendHtml
2121 , removeHTMLConfigs
2222 , FrontendMode (..)
23+ , FrontendWidgetT
2324 , module Obelisk.Frontend.Cookie
2425 ) where
2526
@@ -226,18 +227,17 @@ runFrontendWithConfigsAndCurrentRoute mode configs validFullEncoder frontend = d
226227 then runHydrationWidgetWithHeadAndBody (pure ()) w
227228 else runImmediateWidgetWithHeadAndBody w
228229
230+type FrontendWidgetT r = RoutedT DomTimeline r (SetRouteT DomTimeline r (RouteToUrlT r (ConfigsT (CookiesT (HydratableT (PostBuildT DomTimeline (StaticDomBuilderT DomTimeline (PerformEventT DomTimeline DomHost))))))))
231+
229232renderFrontendHtml
230− :: ( t ~ DomTimeline
231− , MonadIO m
232− , widget ~ RoutedT t r (SetRouteT t r (RouteToUrlT r (ConfigsT (CookiesT (HydratableT (PostBuildT t (StaticDomBuilderT t (PerformEventT t DomHost))))))))
233− )
233+ :: MonadIO m
234234 => Map Text ByteString
235235 -> Cookies
236236 -> (r -> Text)
237237 -> r
238238 -> Frontend r
239− -> widget ()
240− -> widget ()
239+ -> FrontendWidgetT r ()
240+ -> FrontendWidgetT r ()
241241 -> m ByteString
242242renderFrontendHtml configs cookies urlEnc route frontend headExtra bodyExtra = do
243243 --TODO: We should probably have a "NullEventWriterT" or a frozen reflex timeline
lib/run/src/Obelisk/Run.hs+1−1
@@ -97,7 +97,7 @@ run port serveStaticAsset backend frontend = do
9797 Identity r -> case r of
9898 FullRoute_Backend backendRoute :/ a -> serveRoute $ backendRoute :/ a
9999 FullRoute_Frontend obeliskRoute :/ a ->
100− serveDefaultObeliskApp appRouteToUrl allJsUrl serveStaticAsset frontend publicConfigs $ obeliskRoute :/ a
100+ serveDefaultObeliskApp appRouteToUrl (($ allJsUrl) <$> defaultGhcjsWidgets) serveStaticAsset frontend publicConfigs $ obeliskRoute :/ a
101101 where
102102 appRouteToUrl (k :/ v) = renderObeliskRoute validFullEncoder (FullRoute_Frontend (ObeliskRoute_App k) :/ v)
103103 allJsUrl = renderAllJsPath validFullEncoder