on master help sign in
Merge remote-tracking branch 'origin/develop' into eac@ghcide-compat
commit28118b510b770b04ffe4c99d7e6b82afcdcf325c authorElliot Cameron <eacameron@gmail.com> dateThu 2 Apr 2020, 18:16 (7 years ago) parentd7cadcc50b0e58
45 files changed+1158−643
ChangeLog.md+15 README.md+41−19 all-builds.nix+2−4 default.nix+58−96 dep/reflex-platform/github.json+3−3 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+20 haskell-overlays/obelisk.nix+42 haskell-overlays/tighten-ob-exes.nix+32 lib/asset/serve-snap/src/Obelisk/Asset/Serve/Snap.hs+14−4 lib/backend/src/Obelisk/Backend.hs+109−26 lib/cliapp/src/Obelisk/CliApp.hs+1 lib/cliapp/src/Obelisk/CliApp/Process.hs+8−3 lib/command/obelisk-command.cabal+1−3 lib/command/src/Obelisk/Command.hs+25−24 lib/command/src/Obelisk/Command/Deploy.hs+61−60 lib/command/src/Obelisk/Command/Nix.hs+21−11 lib/command/src/Obelisk/Command/Project.hs+25−5 lib/command/src/Obelisk/Command/Run.hs+62−27 lib/command/src/Obelisk/Command/Upgrade/Hash.hs−114 lib/command/src/Obelisk/Command/Utils.hs+3 lib/executable-config/default.nix+23−18 lib/frontend/src/Obelisk/Frontend.hs+6−6 lib/run/src/Obelisk/Run.hs+1−1 lib/selftest/obelisk-selftest.cabal−1 lib/selftest/src/Obelisk/SelfTest.hs+242−218 nixpkgs-overlays/default.nix+15 skeleton/.gitignore+1
ChangeLog.md+15
@@ -2,6 +2,21 @@
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+## 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+
520## v0.6.0.0 - 2020-02-21
621
722* 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
all-builds.nix+2−4
@@ -3,10 +3,7 @@
33 iosSdkVersion = "10.2";
44 }
55, local-self ? import ./. self-args
6−, supportedSystems ? [
7− "x86_64-linux"
8− "x86_64-darwin"
9− ]
6+, supportedSystems ? [ builtins.currentSystem ]
107}:
118
129let
@@ -68,6 +65,7 @@ let
6865 iosSkeleton = (import ./skeleton { inherit obelisk; }).ios.frontend;
6966 nameSuffix = if profiling then "profiled" else "unprofiled";
7067 packages = {
68+ skeletonProfiledObRun = skeleton.__unstable__.profiledObRun;
7169 inherit
7270 command
7371 serverSkeletonShell
default.nix+58−96
@@ -5,103 +5,24 @@
55, reflex-platform-func ? import ./dep/reflex-platform
66}:
77let
8− reflex-platform = getReflexPlatform system;
8+ reflex-platform = getReflexPlatform { inherit system; };
99 inherit (reflex-platform) hackGet nixpkgs;
1010 pkgs = nixpkgs;
1111
1212 inherit (import dep/gitignore.nix { inherit (nixpkgs) lib; }) gitignoreSource;
1313
14− cleanSource = src: gitignoreSource (pkgs.lib.cleanSource src);
15−
16− commandRuntimeDeps = pkgs: with pkgs; [
17− coreutils
18− git
19− nix-prefetch-git
20− openssh
21− ];
22−
23− getReflexPlatform = sys: reflex-platform-func {
24− inherit iosSdkVersion config;
25− system = sys;
26− enableLibraryProfiling = profiling;
14+ getReflexPlatform = { system, enableLibraryProfiling ? profiling }: reflex-platform-func {
15+ inherit iosSdkVersion config system enableLibraryProfiling;
2716
2817 nixpkgsOverlays = [
29− (self: super: {
30− obeliskExecutableConfig = import ./lib/executable-config { nixpkgs = pkgs; filterGitSource = cleanSource; };
31− })
18+ (import ./nixpkgs-overlays)
3219 ];
3320
3421 haskellOverlays = [
35−
36− # Fix misc upstream packages
37− (self: super: let
38− pkgs = self.callPackage ({ pkgs }: pkgs) {};
39− haskellLib = pkgs.haskell.lib;
40− in {
41− hnix = haskellLib.dontCheck (haskellLib.doJailbreak (self.callCabal2nix "hnix" (hackGet dep/hnix) {}));
42− hnix-store-core = self.callHackage "hnix-store-core" "0.1.0.0" {};
43−
44− ghcid = self.callCabal2nix "ghcid" (hackGet ./dep/ghcid) {};
45− })
46−
22+ (import ./haskell-overlays/misc-deps.nix { inherit hackGet; })
4723 pkgs.obeliskExecutableConfig.haskellOverlay
48−
49− # Add obelisk packages
50− (self: super: let
51− pkgs = self.callPackage ({ pkgs }: pkgs) {};
52− onLinux = pkg: f: if pkgs.stdenv.isLinux then f pkg else pkg;
53− in {
54− shelly = self.callHackage "shelly" "1.9.0" {};
55−
56− obelisk-executable-config-inject = pkgs.obeliskExecutableConfig.platforms.web.inject self;
57−
58− obelisk-asset-manifest = self.callCabal2nix "obelisk-asset-manifest" (cleanSource ./lib/asset/manifest) {};
59− obelisk-asset-serve-snap = self.callCabal2nix "obelisk-asset-serve-snap" (cleanSource ./lib/asset/serve-snap) {};
60− obelisk-backend = self.callCabal2nix "obelisk-backend" (cleanSource ./lib/backend) {};
61− obelisk-cliapp = self.callCabal2nix "obelisk-cliapp" (cleanSource ./lib/cliapp) {};
62− obelisk-command = haskellLib.overrideCabal (self.callCabal2nix "obelisk-command" (cleanSource ./lib/command) {}) {
63− librarySystemDepends = [
64− pkgs.nix
65− (haskellLib.justStaticExecutables self.ghcid)
66− ];
67− };
68− obelisk-frontend = self.callCabal2nix "obelisk-frontend" (cleanSource ./lib/frontend) {};
69− obelisk-run = onLinux (self.callCabal2nix "obelisk-run" (cleanSource ./lib/run) {}) (pkg:
70− haskellLib.overrideCabal pkg (drv: { librarySystemDepends = [ pkgs.iproute ]; })
71− );
72− obelisk-route = self.callCabal2nix "obelisk-route" (cleanSource ./lib/route) {};
73− obelisk-selftest = haskellLib.overrideCabal (self.callCabal2nix "obelisk-selftest" (cleanSource ./lib/selftest) {}) {
74− librarySystemDepends = [
75− pkgs.cabal-install
76− pkgs.coreutils
77− pkgs.git
78− pkgs.nix
79− ];
80− };
81− obelisk-snap-extras = self.callCabal2nix "obelisk-snap-extras" (cleanSource ./lib/snap-extras) {};
82− tabulation = self.callCabal2nix "tabulation" (cleanSource ./lib/tabulation) {};
83− })
84−
85− (self: super: let
86− pkgs = self.callPackage ({ pkgs }: pkgs) {};
87− haskellLib = pkgs.haskell.lib;
88− in {
89− # Dynamic linking with split objects dramatically increases startup time (about
90− # 0.5 seconds on a decent machine with SSD), so we do `justStaticExecutables`.
91− obelisk-command = haskellLib.overrideCabal
92− (haskellLib.generateOptparseApplicativeCompletion "ob"
93− (haskellLib.justStaticExecutables super.obelisk-command))
94− (drv: {
95− buildTools = (drv.buildTools or []) ++ [ pkgs.buildPackages.makeWrapper ];
96− postFixup = ''
97− ${drv.postFixup or ""}
98− # Make `ob` reference its runtime dependencies.
99− wrapProgram "$out"/bin/ob --prefix PATH : ${pkgs.lib.makeBinPath (commandRuntimeDeps pkgs)}
100− '';
101− });
102− obelisk-selftest = haskellLib.justStaticExecutables super.obelisk-selftest;
103− })
104−
24+ (import ./haskell-overlays/obelisk.nix)
25+ (import ./haskell-overlays/tighten-ob-exes.nix)
10526 ];
10627 };
10728
@@ -124,7 +45,7 @@ in rec {
12445 obelisk = ghcObelisk;
12546 obeliskEnvs = pkgs.lib.filterAttrs (k: _: pkgs.lib.strings.hasPrefix "obelisk-" k) ghcObeliskEnvs;
12647 command = ghcObelisk.obelisk-command;
127− shell = pinBuildInputs "obelisk-shell" ([command] ++ commandRuntimeDeps pkgs);
48+ shell = pinBuildInputs "obelisk-shell" ([command] ++ command.commandRuntimeDeps);
12849
12950 selftest = pkgs.writeScript "selftest" ''
13051 #!${pkgs.runtimeShell}
@@ -166,18 +87,28 @@ in rec {
16687 '';
16788
16889 serverModules = {
169− mkBaseEc2 = { hostName, routeHost, enableHttps, adminEmail, ... }: {...}: {
90+ mkBaseEc2 = { nixosPkgs, ... }: {...}: {
17091 imports = [
171− (pkgs.path + /nixos/modules/virtualisation/amazon-image.nix)
92+ (nixosPkgs.path + /nixos/modules/virtualisation/amazon-image.nix)
17293 ];
94+ ec2.hvm = true;
95+ };
96+
97+ mkDefaultNetworking = { adminEmail, enableHttps, hostName, routeHost, ... }: {...}: {
17398 networking = {
17499 inherit hostName;
175100 firewall.allowedTCPPorts = if enableHttps then [ 80 443 ] else [ 80 ];
176101 };
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+
177109 security.acme.certs = if enableHttps then {
178110 "${routeHost}".email = adminEmail;
179111 } else {};
180− ec2.hvm = true;
181112 };
182113
183114 mkObeliskApp =
@@ -234,6 +165,8 @@ in rec {
234165 };
235166 };
236167
168+ inherit mkAssets;
169+
237170 serverExe = backend: frontend: assets: optimizationLevel: version:
238171 pkgs.runCommand "serverExe" {} ''
239172 mkdir $out
@@ -244,14 +177,15 @@ in rec {
244177 echo ${version} > $out/version
245178 '';
246179
247− server = { exe, hostName, adminEmail, routeHost, enableHttps, version }@args:
180+ server = { exe, hostName, adminEmail, routeHost, enableHttps, version, module ? serverModules.mkBaseEc2 }@args:
248181 let
249182 nixos = import (pkgs.path + /nixos);
250183 in nixos {
251184 system = "x86_64-linux";
252185 configuration = {
253186 imports = [
254− (serverModules.mkBaseEc2 args)
187+ (module { inherit exe hostName adminEmail routeHost enableHttps version; nixosPkgs = pkgs; })
188+ (serverModules.mkDefaultNetworking args)
255189 (serverModules.mkObeliskApp args)
256190 ];
257191 };
@@ -260,7 +194,7 @@ in rec {
260194 # An Obelisk project is a reflex-platform project with a predefined layout and role for each component
261195 project = base': projectDefinition:
262196 let
263− projectOut = sys: let reflexPlatformProject = (getReflexPlatform sys).project; in reflexPlatformProject (args@{ nixpkgs, ... }:
197+ projectOut = { system, enableLibraryProfiling ? profiling }: let reflexPlatformProject = (getReflexPlatform { inherit system enableLibraryProfiling; }).project; in reflexPlatformProject (args@{ nixpkgs, ... }:
264198 let
265199 inherit (lib.strings) hasPrefix;
266200 mkProject =
@@ -365,20 +299,48 @@ in rec {
365299 });
366300 in allConfig;
367301 in (mkProject (projectDefinition args)).projectConfig);
368− mainProjectOut = projectOut system;
302+ mainProjectOut = projectOut { inherit system; };
369303 serverOn = projectInst: version: serverExe
370304 projectInst.ghc.backend
371305 mainProjectOut.ghcjs.frontend
372306 projectInst.passthru.staticFiles
373307 projectInst.passthru.__closureCompilerOptimizationLevel
374308 version;
375− linuxExe = serverOn (projectOut "x86_64-linux");
309+ linuxExe = serverOn (projectOut { system = "x86_64-linux"; });
376310 dummyVersion = "Version number is only available for deployments";
377311 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+
378340 linuxExeConfigurable = linuxExe;
379341 linuxExe = linuxExe dummyVersion;
380342 exe = serverOn mainProjectOut dummyVersion;
381− server = args@{ hostName, adminEmail, routeHost, enableHttps, version }:
343+ server = args@{ hostName, adminEmail, routeHost, enableHttps, version, module ? serverModules.mkBaseEc2 }:
382344 server (args // { exe = linuxExe version; });
383345 obelisk = import (base' + "/.obelisk/impl") {};
384346 };
dep/reflex-platform/github.json+3−3
@@ -3,6 +3,6 @@
33 "repo": "reflex-platform",
44 "branch": "master",
55 "private": false,
6− "rev": "5429278830e1555a577f2550e045ce7f7164aa65",
7− "sha256": "1lp86cgccmim573rarsjny5vh0ygkfp5afq7006li0k9w2sw2d4c"
8−}
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.nixnew file+20
@@ -0,0 +1,20 @@
1+{ hackGet }:
2+
3+# Fix misc upstream packages
4+self: super:
5+
6+let
7+ pkgs = self.callPackage ({ pkgs }: pkgs) {};
8+ haskellLib = pkgs.haskell.lib;
9+in
10+
11+{
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" {};
20+}
haskell-overlays/obelisk.nixnew file+42
@@ -0,0 +1,42 @@
1+# Add obelisk packages
2+
3+self: super:
4+
5+let
6+ pkgs = self.callPackage ({ pkgs }: pkgs) {};
7+ inherit (pkgs) obeliskCleanSource;
8+ haskellLib = pkgs.haskell.lib;
9+ onLinux = pkg: f: if pkgs.stdenv.isLinux then f pkg else pkg;
10+in
11+
12+{
13+ obelisk-executable-config-inject = pkgs.obeliskExecutableConfig.platforms.web.inject self;
14+
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) {};
42+}
haskell-overlays/tighten-ob-exes.nixnew file+32
@@ -0,0 +1,32 @@
1+self: super:
2+
3+let
4+ pkgs = self.callPackage ({ pkgs }: pkgs) {};
5+ haskellLib = pkgs.haskell.lib;
6+
7+ commandRuntimeDeps = with pkgs; [
8+ coreutils
9+ git
10+ nix-prefetch-git
11+ openssh
12+ ];
13+in
14+
15+{
16+ # Dynamic linking with split objects dramatically increases startup time (about
17+ # 0.5 seconds on a decent machine with SSD), so we do `justStaticExecutables`.
18+ obelisk-command = haskellLib.overrideCabal
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+ });
30+
31+ obelisk-selftest = haskellLib.justStaticExecutables super.obelisk-selftest;
32+}
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+109−26
@@ -1,3 +1,5 @@
1+{-# LANGUAGE DeriveFunctor #-}
2+{-# LANGUAGE DeriveGeneric #-}
13{-# LANGUAGE FlexibleContexts #-}
24{-# LANGUAGE FlexibleInstances #-}
35{-# LANGUAGE GADTs #-}
@@ -9,15 +11,30 @@
911{-# 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− , renderAllJsPath
20− , runBackend
2138 , staticRenderContentType
2239 , getPublicConfigs
2340 ) where
@@ -35,6 +52,7 @@ import qualified Data.Map as Map
3552import Data.Monoid ((<>))
3653import Data.Text (Text)
3754import qualified Data.Text as T
55+import GHC.Generics (Generic)
3856import Obelisk.Asset.Serve.Snap (serveAsset)
3957import qualified Obelisk.ExecutableConfig.Lookup as Lookup
4058import Obelisk.Frontend
@@ -50,12 +68,37 @@ import System.IO (BufferMode (..), hSetBuffering, stderr, stdout)
5068data Backend backendRoute frontendRoute = Backend
5169 { _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
@@ -63,14 +106,14 @@ data GhcjsApp route = GhcjsApp
63106serveDefaultObeliskApp
64107 :: (MonadSnap m, HasCookies m, MonadFail m)
65108 => (R appRoute -> Text)
66− -> Text -- ^ URL to GHCJS app JavaScript
109+ -> GhcjsWidgets (FrontendWidgetT (R appRoute) ())
67110 -> ([Text] -> m ())
68111 -> Frontend (R appRoute)
69112 -> Map Text ByteString
70113 -> R (ObeliskRoute appRoute)
71114 -> m ()
72−serveDefaultObeliskApp urlEnc allJsUrl serveStaticAsset frontend =
73− serveObeliskApp urlEnc allJsUrl serveStaticAsset frontendApp
115+serveDefaultObeliskApp urlEnc ghcjsWidgets serveStaticAsset frontend =
116+ serveObeliskApp urlEnc ghcjsWidgets serveStaticAsset frontendApp
74117 where frontendApp = GhcjsApp
75118 { _ghcjsApp_compiled = defaultFrontendGhcjsAssets
76119 , _ghcjsApp_value = frontend
@@ -95,17 +138,20 @@ defaultFrontendGhcjsAssets = StaticAssets
95138 , _staticAssets_unprocessed = "frontend.jsexe"
96139 }
97140
98−runSnapWithCommandLineArgs :: MonadIO m => Snap () -> m ()
99−runSnapWithCommandLineArgs a = do
100− -- Get the web server configuration from the command line
101− cmdLineConf <- liftIO $ commandLineConfig defaultConfig
102− let httpConf = cmdLineConf
141+runSnapWithConfig :: MonadIO m => Config Snap a -> Snap () -> m ()
142+runSnapWithConfig conf a = do
143+ let httpConf = conf
103144 { accessLog = Just $ ConfigIoLog BSC8.putStrLn
104145 , errorLog = Just $ ConfigIoLog BSC8.putStrLn
105146 }
106147 -- Start the web server
107148 liftIO $ httpServe httpConf a
108149
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
154+
109155getPageName :: (MonadSnap m) => m PageName
110156getPageName = do
111157 p <- getsRequest rqPathInfo
@@ -124,17 +170,17 @@ renderAllJsPath validFullEncoder =
124170serveObeliskApp
125171 :: (MonadSnap m, HasCookies m, MonadFail m)
126172 => (R appRoute -> Text)
127− -> Text -- ^ URL to GHCJS app JavaScript
173+ -> GhcjsWidgets (FrontendWidgetT (R appRoute) ())
128174 -> ([Text] -> m ())
129175 -> GhcjsApp (R appRoute)
130176 -> Map Text ByteString
131177 -> R (ObeliskRoute appRoute)
132178 -> m ()
133−serveObeliskApp urlEnc allJsUrl serveStaticAsset frontendApp config = \case
134− ObeliskRoute_App appRouteComponent :=> Identity appRouteRest -> serveGhcjsApp urlEnc allJsUrl frontendApp config $ GhcjsAppRoute_App appRouteComponent :/ appRouteRest
179+serveObeliskApp urlEnc ghcjsWidgets serveStaticAsset frontendApp config = \case
180+ ObeliskRoute_App appRouteComponent :=> Identity appRouteRest -> serveGhcjsApp urlEnc ghcjsWidgets frontendApp config $ GhcjsAppRoute_App appRouteComponent :/ appRouteRest
135181 ObeliskRoute_Resource resComponent :=> Identity resRest -> case resComponent :=> Identity resRest of
136182 ResourceRoute_Static :=> Identity pathSegments -> serveStaticAsset pathSegments
137− ResourceRoute_Ghcjs :=> Identity pathSegments -> serveGhcjsApp urlEnc allJsUrl frontendApp config $ GhcjsAppRoute_Resource :/ pathSegments
183+ ResourceRoute_Ghcjs :=> Identity pathSegments -> serveGhcjsApp urlEnc ghcjsWidgets frontendApp config $ GhcjsAppRoute_Resource :/ pathSegments
138184 ResourceRoute_JSaddleWarp :=> Identity _ -> do
139185 let msg = "Error: Obelisk.Backend received jsaddle request"
140186 liftIO $ putStrLn $ T.unpack msg
@@ -161,30 +207,43 @@ staticRenderContentType = "text/html; charset=utf-8"
161207serveGhcjsApp
162208 :: (MonadSnap m, HasCookies m, MonadFail m)
163209 => (R appRouteComponent -> Text)
164− -> Text -- ^ URL to GHCJS app JavaScript
210+ -> GhcjsWidgets (FrontendWidgetT (R appRouteComponent) ())
165211 -> GhcjsApp (R appRouteComponent)
166212 -> Map Text ByteString
167213 -> R (GhcjsAppRoute appRouteComponent)
168214 -> m ()
169−serveGhcjsApp urlEnc allJsUrl app config = \case
215+serveGhcjsApp urlEnc ghcjsWidgets app config = \case
170216 GhcjsAppRoute_App appRouteComponent :=> Identity appRouteRest -> do
171217 modifyResponse $ setContentType staticRenderContentType
172218 modifyResponse $ setHeader "Cache-Control" "no-store private"
173− writeBS <=< renderGhcjsFrontend urlEnc allJsUrl (appRouteComponent :/ appRouteRest) config $ _ghcjsApp_value app
219+ writeBS <=< renderGhcjsFrontend urlEnc ghcjsWidgets (appRouteComponent :/ appRouteRest) config $ _ghcjsApp_value app
174220 GhcjsAppRoute_Resource :=> Identity pathSegments -> serveStaticAssets (_ghcjsApp_compiled app) pathSegments
175221
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.
176227runBackend :: Backend backendRoute frontendRoute -> Frontend (R frontendRoute) -> IO ()
177−runBackend backend frontend = case checkEncoder $ _backend_routeEncoder backend of
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
178237 Left e -> fail $ "backend error:\n" <> T.unpack e
179238 Right validFullEncoder -> do
180239 publicConfigs <- getPublicConfigs
181240 _backend_run backend $ \serveRoute ->
182− runSnapWithCommandLineArgs $
241+ runSnap $
183242 getRouteWith validFullEncoder >>= \case
184243 Identity r -> case r of
185244 FullRoute_Backend backendRoute :/ a -> serveRoute $ backendRoute :/ a
186245 FullRoute_Frontend obeliskRoute :/ a ->
187− serveDefaultObeliskApp routeToUrl allJsUrl (serveStaticAssets defaultStaticAssets) frontend publicConfigs $
246+ serveDefaultObeliskApp routeToUrl (($ allJsUrl) <$> ghcjsWidgets) (serveStaticAssets staticAssets) frontend publicConfigs $
188247 obeliskRoute :/ a
189248 where
190249 routeToUrl (k :/ v) = renderObeliskRoute validFullEncoder $ FullRoute_Frontend (ObeliskRoute_App k) :/ v
@@ -193,16 +252,40 @@ runBackend backend frontend = case checkEncoder $ _backend_routeEncoder backend
193252renderGhcjsFrontend
194253 :: (MonadSnap m, HasCookies m)
195254 => (route -> Text)
196− -> Text -- ^ URL to GHCJS app JavaScript
255+ -> GhcjsWidgets (FrontendWidgetT route ())
197256 -> route
198257 -> Map Text ByteString
199258 -> Frontend route
200259 -> 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
260+renderGhcjsFrontend urlEnc ghcjsWidgets route configs f = do
204261 cookies <- askCookies
205− renderFrontendHtml configs cookies urlEnc route f ghcjsPreload ghcjsScript
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+ ]
206289
207290instance HasCookies Snap where
208291 askCookies = map (\c -> (cookieName c, cookieValue c)) <$> getsRequest rqCookies
lib/cliapp/src/Obelisk/CliApp.hs+1
@@ -51,6 +51,7 @@ module Obelisk.CliApp
5151 , setDelegateCtlc
5252 , setEnvOverride
5353 , shell
54+ , waitForProcess
5455 ) where
5556
5657import Control.Monad.Log (Severity (..))
lib/cliapp/src/Obelisk/CliApp/Process.hs+8−3
@@ -28,6 +28,7 @@ module Obelisk.CliApp.Process
2828 , setDelegateCtlc
2929 , setEnvOverride
3030 , shell
31+ , waitForProcess
3132 ) where
3233
3334import Control.Monad ((<=<), join, void)
@@ -51,7 +52,7 @@ import System.IO (Handle)
5152import System.IO.Streams (InputStream, handleToInputStream)
5253import qualified System.IO.Streams as Streams
5354import System.IO.Streams.Concurrent (concurrentMerge)
54−import System.Process (CreateProcess, ProcessHandle, StdStream (CreatePipe), std_err, std_out, waitForProcess)
55+import System.Process (CreateProcess, ProcessHandle, StdStream (CreatePipe), std_err, std_out)
5556import qualified System.Process as Process
5657import qualified Data.Aeson as Aeson
5758
@@ -143,7 +144,7 @@ readProcessAndLogOutput (sev_out, sev_err) process = do
143144 outText <- liftIO $ T.decodeUtf8With lenientDecode <$> BS.hGetContents out
144145 putLogRaw sev_out outText
145146
146− liftIO (waitForProcess p) >>= \case
147+ waitForProcess p >>= \case
147148 ExitSuccess -> pure outText
148149 ExitFailure code -> throwError $ review asProcessFailure $ ProcessFailure (Process.cmdspec $ _processSpec_createProcess process) code
149150
@@ -218,7 +219,7 @@ withProcess process f = do -- TODO: Use bracket.
218219
219220 f out err -- Pass the handles to the passed function
220221
221− liftIO (waitForProcess p) >>= \case
222+ waitForProcess p >>= \case
222223 ExitSuccess -> return (out, err)
223224 ExitFailure code -> throwError $ review asProcessFailure $ ProcessFailure (Process.cmdspec $ _processSpec_createProcess process) code
224225
@@ -235,6 +236,10 @@ streamToLog stream = fix $ \loop -> do
235236 Nothing -> return ()
236237 Just (sev, line) -> putLogRaw sev (T.decodeUtf8With lenientDecode line) >> loop
237238
239+-- | Wrapper around `System.Process.waitForProcess`
240+waitForProcess :: MonadIO m => ProcessHandle -> m ExitCode
241+waitForProcess = liftIO . Process.waitForProcess
242+
238243-- | Pretty print a 'CmdSpec'
239244reconstructCommand :: Process.CmdSpec -> Text
240245reconstructCommand p = case p of
lib/command/obelisk-command.cabal+1−3
@@ -1,5 +1,5 @@
11name: obelisk-command
2−version: 0.6
2+version: 0.7.0.1
33cabal-version: >= 1.8
44build-type: Simple
55
@@ -10,8 +10,6 @@ library
1010 , aeson-pretty
1111 , ansi-terminal
1212 , base
13− , base16-bytestring
14− , binary
1513 , bytestring
1614 , Cabal
1715 , containers
lib/command/src/Obelisk/Command.hs+25−24
@@ -7,16 +7,10 @@ module Obelisk.Command where
77
88import Control.Monad
99import Control.Monad.IO.Class (liftIO)
10−import qualified Data.Binary as Binary
1110import Data.Bool (bool)
12−import qualified Data.ByteString.Base16 as Base16
13−import qualified Data.ByteString.Lazy as LBS
1411import Data.Foldable (for_)
1512import Data.List
1613import qualified Data.Text as T
17−import Data.Text.Encoding
18−import Data.Text.Encoding.Error (lenientDecode)
19−import GHC.StaticPtr
2014import Options.Applicative
2115import Options.Applicative.Help.Pretty (text, (<$$>))
2216import System.Directory
@@ -87,6 +81,7 @@ data ObCommand
8781 = ObCommand_Init InitSource Bool
8882 | ObCommand_Deploy DeployCommand
8983 | ObCommand_Run
84+ | ObCommand_Profile String [String]
9085 | ObCommand_Thunk ThunkCommand
9186 | ObCommand_Repl
9287 | ObCommand_Watch
@@ -109,6 +104,7 @@ obCommand cfg = hsubparser
109104 [ command "init" $ info (ObCommand_Init <$> initSource <*> initForce) $ progDesc "Initialize an Obelisk project"
110105 , command "deploy" $ info (ObCommand_Deploy <$> deployCommand cfg) $ progDesc "Prepare a deployment for an Obelisk project"
111106 , command "run" $ info (pure ObCommand_Run) $ progDesc "Run current project in development mode"
107+ , command "profile" $ info (uncurry ObCommand_Profile <$> profileCommand) $ progDesc "Run current project with profiling enabled"
112108 , command "thunk" $ info (ObCommand_Thunk <$> thunkCommand) $ progDesc "Manipulate thunk directories"
113109 , command "repl" $ info (pure ObCommand_Repl) $ progDesc "Open an interactive interpreter"
114110 , command "watch" $ info (pure ObCommand_Watch) $ progDesc "Watch current project for errors and warnings"
@@ -140,7 +136,7 @@ deployCommand cfg = hsubparser $ mconcat
140136 where
141137 platformP = hsubparser $ mconcat
142138 [ command "android" $ info (pure (Android, [])) mempty
143− , command "ios" $ info ((,) <$> pure IOS <*> fmap pure (strArgument (metavar "TEAMID" <> help "Your Team ID - found in the Apple developer portal"))) mempty
139+ , command "ios" $ info ((,) <$> pure IOS <*> fmap pure (strArgument (metavar "TEAMID" <> help "Your Team ID - found in the Apple developer portal"))) mempty
144140 ]
145141
146142 remoteBuilderParser :: Parser (Maybe RemoteBuilder)
@@ -162,8 +158,8 @@ deployCommand cfg = hsubparser $ mconcat
162158
163159deployInitOpts :: Parser DeployInitOpts
164160deployInitOpts = DeployInitOpts
165− <$> strArgument (action "directory" <> metavar "DEPLOYDIR" <> help "Path to a directory that it will create")
166− <*> strOption (long "ssh-key" <> action "file" <> metavar "SSHKEY" <> help "Path to an ssh key that it will symlink to")
161+ <$> strArgument (action "directory" <> metavar "DEPLOYDIR" <> help "Path to a directory where the deployment repository will be initialized")
162+ <*> strOption (long "ssh-key" <> action "file" <> metavar "SSHKEY" <> help "Path to an SSH key that will be *copied* to the deployment repository")
167163 <*> some (strOption (long "hostname" <> metavar "HOSTNAME" <> help "hostname of the deployment target"))
168164 <*> strOption (long "route" <> metavar "PUBLICROUTE" <> help "Publicly accessible URL of your app")
169165 <*> strOption (long "admin-email" <> metavar "ADMINEMAIL" <> help "Email address where administrative alerts will be sent")
@@ -190,6 +186,25 @@ data DeployInitOpts = DeployInitOpts
190186 }
191187 deriving Show
192188
189+profileCommand :: Parser (String, [String])
190+profileCommand = (,)
191+ <$> strOption
192+ ( long "output"
193+ <> short 'o'
194+ <> 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."
195+ <> metavar "PATH"
196+ <> value "profile/%Y-%m-%dT%H:%M:%S"
197+ <> showDefault
198+ )
199+ <*> (words <$> strOption
200+ ( long "rts-flags"
201+ <> help "RTS Flags to pass to the executable."
202+ <> value "-p -hc"
203+ <> metavar "FLAGS"
204+ <> showDefault
205+ ))
206+
207+
193208--TODO: Result should provide normalised path and also original user input for error reporting.
194209thunkDirectoryParser :: Parser FilePath
195210thunkDirectoryParser = fmap (dropTrailingPathSeparator . normalise) . strArgument $ mconcat
@@ -260,9 +275,6 @@ parserPrefs = defaultPrefs
260275 { prefShowHelpOnEmpty = True
261276 }
262277
263−parseCLIArgs :: ArgsConfig -> [String] -> IO Args
264−parseCLIArgs cfg = handleParseResult . execParserPure parserPrefs (argsInfo cfg)
265−
266278-- | Create an Obelisk config for the current process.
267279mkObeliskConfig :: IO Obelisk
268280mkObeliskConfig = do
@@ -378,6 +390,7 @@ ob = \case
378390 DeployCommand_Update -> deployUpdate "."
379391 DeployCommand_Test (platform, extraArgs) -> deployMobile platform extraArgs
380392 ObCommand_Run -> run
393+ ObCommand_Profile basePath rtsFlags -> profile basePath rtsFlags
381394 ObCommand_Thunk tc -> case tc of
382395 ThunkCommand_Update thunks config -> for_ thunks (updateThunkToLatest config)
383396 ThunkCommand_Unpack thunks -> for_ thunks unpackThunk
@@ -409,15 +422,3 @@ haddockCommand pkgs = unwords
409422
410423getArgsConfig :: IO ArgsConfig
411424getArgsConfig = pure $ ArgsConfig { _argsConfig_enableVmBuilderByDefault = System.Info.os == "darwin" }
412−
413−encodeStaticKey :: StaticKey -> String
414−encodeStaticKey = T.unpack . decodeUtf8With lenientDecode . Base16.encode . LBS.toStrict . Binary.encode
415−
416−-- TODO: Use failWith in place of fail to be consistent.
417−decodeStaticKey :: String -> Either String StaticKey
418−decodeStaticKey s = case Base16.decode $ encodeUtf8 $ T.pack s of
419− (b, "") -> case Binary.decodeOrFail $ LBS.fromStrict b of
420− Right ("", _, a) -> pure a
421− Right _ -> fail "decodeStaticKey: Binary.decodeOrFail didn't consume all input"
422− Left (_, _, e) -> fail $ "decodeStaticKey: Binary.decodeOrFail failed: " <> show e
423− _ -> fail $ "decodeStaticKey: could not decode hex string: " <> show s
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/Nix.hs+21−11
@@ -32,6 +32,8 @@ module Obelisk.Command.Nix
3232
3333 , boolArg
3434 , nixCmd
35+ , nixCmdProc
36+ , nixCmdProc'
3537 , rawArg
3638 , runNixShellConfig
3739 , strArg
@@ -188,27 +190,35 @@ runNixShellConfig cfg = mconcat
188190 ["--run", run] | run <- maybeToList $ cfg ^. nixShellConfig_run
189191 ]
190192
191−nixCmd :: MonadObelisk m => NixCmd -> m FilePath
192−nixCmd cmdCfg = withSpinner' ("Running " <> cmd <> desc) (Just $ const $ "Built " <> desc) $ do
193− output <- readProcessAndLogStderr Debug $ proc (T.unpack cmd) options
194− -- Remove final newline that Nix appends
195− Just (outPath, '\n') <- pure $ T.unsnoc output
196− pure $ T.unpack outPath
193+nixCmdProc :: NixCmd -> ProcessSpec
194+nixCmdProc = fst . nixCmdProc'
195+
196+nixCmdProc' :: NixCmd -> (ProcessSpec, T.Text)
197+nixCmdProc' cmdCfg = (proc (T.unpack cmd) options, cmd)
197198 where
198− (cmd, options, commonCfg) = case cmdCfg of
199+ (cmd, options) = case cmdCfg of
199200 NixCmd_Build cfg' ->
200201 ( "nix-build"
201202 , runNixBuildConfig cfg'
202− , cfg' ^. nixCommonConfig
203203 )
204204 NixCmd_Instantiate cfg' ->
205205 ( "nix-instantiate"
206206 , runNixInstantiateConfig cfg'
207− , cfg' ^. nixCommonConfig
208207 )
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
209220 path = commonCfg ^. nixCmdConfig_target . target_path
210221 desc = T.pack $ mconcat $ catMaybes
211− [ (" on " <>) <$> path
222+ [ ("on " <>) <$> path
212223 , (\a -> " [" <> a <> "]") <$> (commonCfg ^. nixCmdConfig_target . target_attr)
213224 ]
214−
lib/command/src/Obelisk/Command/Project.hs+25−5
@@ -5,6 +5,7 @@ module Obelisk.Command.Project
55 ( InitSource (..)
66 , findProjectObeliskCommand
77 , findProjectRoot
8+ , findProjectAssets
89 , initProject
910 , nixShellRunConfig
1011 , nixShellRunProc
@@ -41,7 +42,6 @@ import System.IO.Temp
4142import System.IO.Unsafe (unsafePerformIO)
4243import System.Posix (FileStatus, FileMode, CMode (..), UserID, deviceID, fileID, fileMode, fileOwner, getFileStatus, getRealUserID)
4344import System.Posix.Files
44−import System.Process (waitForProcess)
4545
4646import GitHub.Data.GitData (Branch)
4747import GitHub.Data.Name (Name)
@@ -50,7 +50,7 @@ import Obelisk.App (MonadObelisk)
5050import Obelisk.CliApp
5151import Obelisk.Command.Nix
5252import Obelisk.Command.Thunk
53−import Obelisk.Command.Utils (nixExePath)
53+import Obelisk.Command.Utils (nixExePath, nixBuildExePath)
5454
5555--TODO: Make this module resilient to random exceptions
5656
@@ -287,7 +287,7 @@ nixShellWithPkgs root isPure chdirToRoot packageNamesAndPaths command = do
287287 [ rawArg "root" $ toNixPath $ if chdirToRoot then "." else root
288288 , strArg "pkgs" (T.unpack $ decodeUtf8 $ BSL.toStrict $ Json.encode packageNamesAndAbsPaths)
289289 ]
290− void $ liftIO $ waitForProcess ph
290+ void $ waitForProcess ph
291291
292292nixShellWithHoogle :: MonadObelisk m => FilePath -> Bool -> String -> Maybe String -> m ()
293293nixShellWithHoogle root isPure shell' command = do
@@ -298,7 +298,7 @@ nixShellWithHoogle root isPure shell' command = do
298298 \userSettings = super.userSettings // { withHoogle = true; };\
299299 \})).project.shells.${shell}"
300300 & nixShellConfig_common . nixCmdConfig_args .~ [ strArg "shell" shell' ]
301− void $ liftIO $ waitForProcess ph
301+ void $ waitForProcess ph
302302
303303projectShell :: MonadObelisk m => FilePath -> Bool -> String -> Maybe String -> m ()
304304projectShell root isPure shellName command = do
@@ -306,4 +306,24 @@ projectShell root isPure shellName command = do
306306 (_, _, _, ph) <- createProcess_ "runNixShellAttr" $ setCwd (Just root) $ nixShellRunProc $ defShellConfig
307307 & nixShellConfig_common . nixCmdConfig_target . target_path ?~ "default.nix"
308308 & nixShellConfig_common . nixCmdConfig_target . target_attr ?~ ("shells." <> shellName)
309− void $ liftIO $ waitForProcess ph
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+62−27
@@ -4,16 +4,18 @@
44{-# LANGUAGE OverloadedStrings #-}
55{-# LANGUAGE ScopedTypeVariables #-}
66{-# LANGUAGE TupleSections #-}
7+{-# LANGUAGE QuasiQuotes #-}
78module Obelisk.Command.Run where
89
910import Control.Arrow ((&&&))
1011import Control.Exception (Exception, bracket)
11−import Control.Lens (ifor)
12−import Control.Monad (filterM, unless)
12+import Control.Lens (ifor, (.~), (&))
13+import Control.Monad (filterM, unless, void)
1314import Control.Monad.Except (runExceptT, throwError)
1415import Control.Monad.IO.Class (liftIO)
1516import Control.Monad.Reader (MonadIO)
1617import Data.Coerce (coerce)
18+import Data.Default (def)
1719import Data.Either
1820import Data.Foldable (for_, toList)
1921import qualified Data.List.NonEmpty as NE
@@ -23,6 +25,8 @@ import Data.Maybe
2325import qualified Data.Set as Set
2426import Data.Text (Text)
2527import qualified Data.Text as T
28+import Data.Time.Clock (getCurrentTime)
29+import Data.Time.Format (formatTime, defaultTimeLocale)
2630import Data.Traversable (for)
2731import Debug.Trace (trace)
2832import Distribution.Compiler (CompilerFlavor(..))
@@ -47,10 +51,23 @@ import qualified System.Info
4751import System.IO.Temp (withSystemTempDirectory)
4852
4953import Obelisk.App (MonadObelisk)
50−import Obelisk.CliApp (Severity (..) , failWith, putLog, proc, readCreateProcessWithExitCode, readProcessAndLogStderr, setCwd)
51−import Obelisk.Command.Project (nixShellWithPkgs, toImplDir, withProjectRoot)
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)
5269import Obelisk.Command.Thunk (attrCacheFileName)
53−import Obelisk.Command.Utils (findExePath, ghcidExePath, nixBuildExePath, nixExePath)
70+import Obelisk.Command.Utils (findExePath, ghcidExePath)
5471
5572data CabalPackageInfo = CabalPackageInfo
5673 { _cabalPackageInfo_packageFile :: FilePath
@@ -71,30 +88,49 @@ data CabalPackageInfo = CabalPackageInfo
7188preprocessorIdentifier :: String
7289preprocessorIdentifier = "__preprocessor-apply-packages"
7390
74−run :: MonadObelisk m => m ()
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 ()
75128run = withProjectRoot "." $ \root -> do
76129 pkgs <- fmap toList . parsePackagesOrFail =<< getLocalPkgs root
77130 withGhciScript pkgs root $ \dotGhciPath -> do
78− assets <- do
79− isDerivation <- readProcessAndLogStderr Debug $ setCwd (Just root) $
80− proc nixExePath
81− [ "eval"
82− , "(let a = import ./. {}; in toString (a.reflex.nixpkgs.lib.isDerivation a.passthru.staticFilesImpure))"
83− , "--raw"
84− -- `--raw` is not available with old nix-instantiate. It drops quotation
85− -- marks and trailing newline, so is very convenient for shelling out.
86− ]
87− -- Check whether the impure static files are a derivation (and so must be built)
88− if isDerivation == "1"
89− then fmap T.strip $ readProcessAndLogStderr Debug $ setCwd (Just root) $ -- Strip whitespace here because nix-build has no --raw option
90− proc nixBuildExePath
91− [ "--no-out-link"
92− , "-E", "(import ./. {}).passthru.staticFilesImpure"
93− ]
94− else readProcessAndLogStderr Debug $ setCwd (Just root) $
95− proc nixExePath ["eval", "-f", ".", "passthru.staticFilesImpure", "--raw"]
96− putLog Debug $ "Assets impurely loaded from: " <> assets
97131 freePort <- getFreePort
132+ assets <- findProjectAssets root
133+ putLog Debug $ "Assets impurely loaded from: " <> assets
98134 runGhcid root True dotGhciPath pkgs $ Just $ unwords
99135 [ "Obelisk.Run.run"
100136 , show freePort
@@ -297,8 +333,7 @@ withGhciScript packageInfos pathBase f = do
297333 , if null modulesToLoad then "" else ":load " <> unwords modulesToLoad
298334 , "import qualified Obelisk.Run"
299335 , "import qualified Frontend"
300− , "import qualified Backend"
301− ]
336+ , "import qualified Backend" ]
302337 withSystemTempDirectory "ob-ghci" $ \fp -> do
303338 let dotGhciPath = fp </> ".ghci"
304339 liftIO $ writeFile dotGhciPath dotGhci
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+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/executable-config/default.nix+23−18
@@ -2,26 +2,31 @@
22# configuration information into a canonical location. It also provides a haskell
33# package that can be used to retrieve the injected configuration on each supported
44# platform.
5−{ nixpkgs
6−, filterGitSource # TODO define this in obelisk
5+{ lib
6+, runCommand
7+, obeliskCleanSource
78}:
8−let injectConfig = config: assets: nixpkgs.runCommand "inject-config" {} (''
9− set -x
10− mkdir -p $out
11− cp --no-preserve=mode -Lr "${assets}" $out/static
12− chmod +w "$out"
13− '' + nixpkgs.lib.optionalString (!(builtins.isNull config)) ''
14− if ! mkdir $out/config; then
15− 2>&1 echo config directory already exists or could not be created
16− exit 1
17− fi
18− cp -a "${config}"/* "$out/config"
19− find "$out/config" -type f -printf '%P\0' >"$out/config.files"
20− '');
21−in with nixpkgs.haskell.lib; {
9+
10+let
11+ injectConfig = config: assets: runCommand "inject-config" {} (''
12+ set -x
13+ mkdir -p $out
14+ cp --no-preserve=mode -Lr "${assets}" $out/static
15+ chmod +w "$out"
16+ '' + lib.optionalString (!(builtins.isNull config)) ''
17+ if ! mkdir $out/config; then
18+ 2>&1 echo config directory already exists or could not be created
19+ exit 1
20+ fi
21+ cp -a "${config}"/* "$out/config"
22+ '');
23+in
24+
25+{
2226 haskellOverlay = self: super: {
23− obelisk-executable-config-lookup = self.callCabal2nix "obelisk-executable-config-lookup" (filterGitSource ./lookup) {};
27+ obelisk-executable-config-lookup = self.callCabal2nix "obelisk-executable-config-lookup" (obeliskCleanSource ./lookup) {};
2428 };
29+
2530 platforms = {
2631 android = {
2732 # Inject the given config directory into an android assets folder
@@ -32,7 +37,7 @@ in with nixpkgs.haskell.lib; {
3237 inject = injectConfig;
3338 };
3439 web = {
35− inject = self: self.callCabal2nix "obelisk-executable-config-inject" (filterGitSource ./inject) {};
40+ inject = self: self.callCabal2nix "obelisk-executable-config-inject" (obeliskCleanSource ./inject) {};
3641 };
3742 };
3843}
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
lib/selftest/obelisk-selftest.cabal−1
@@ -17,7 +17,6 @@ library
1717 , http-types
1818 , HUnit
1919 , network
20− , obelisk-cliapp
2120 , obelisk-executable-config-lookup
2221 , obelisk-run
2322 , which
lib/selftest/src/Obelisk/SelfTest.hs+242−218
@@ -4,11 +4,13 @@
44{-# LANGUAGE OverloadedStrings #-}
55{-# LANGUAGE PatternGuards #-}
66{-# LANGUAGE ScopedTypeVariables #-}
7−{-# LANGUAGE ViewPatterns #-}
87{-# LANGUAGE TemplateHaskell #-}
8+{-# LANGUAGE TypeApplications #-}
9+{-# LANGUAGE ViewPatterns #-}
910module Obelisk.SelfTest where
1011
11−import Control.Exception (bracket, throw)
12+import Control.Concurrent (threadDelay)
13+import Control.Exception (bracket, throw, try)
1214import Control.Monad
1315import Control.Monad.IO.Class
1416import Data.Bool (bool)
@@ -17,26 +19,24 @@ import Data.Semigroup ((<>))
1719import qualified Data.Set as Set
1820import Data.String
1921import Data.Text (Text)
20−import Data.Void
2122import qualified Data.Text as T
2223import qualified Data.Text.IO as T
2324import qualified Network.HTTP.Client as HTTP
2425import qualified Network.HTTP.Types as HTTP
2526import qualified Network.Socket as Socket
26−import Shelly hiding (cp)
27+import Shelly
2728import System.Directory (getCurrentDirectory, withCurrentDirectory, getDirectoryContents)
2829import System.Environment
2930import System.Exit (ExitCode (..))
30−import System.Which (staticWhich)
31+import System.FilePath (addTrailingPathSeparator)
3132import qualified System.Info
3233import System.IO (Handle, hClose, hIsEOF, hGetContents)
3334import System.IO.Temp
34−import System.Process (readProcessWithExitCode, CreateProcess(cwd), readCreateProcessWithExitCode, proc)
35+import System.Process (readProcessWithExitCode)
36+import System.Which (staticWhich)
3537import Test.Hspec
3638import Test.HUnit.Base
3739
38−import Obelisk.CliApp hiding (runCli, readCreateProcessWithExitCode)
39−import qualified Obelisk.CliApp as CliApp
4040import Obelisk.ExecutableConfig.Lookup (getConfigs)
4141import Obelisk.Run (getConfigRoute)
4242
@@ -46,8 +46,8 @@ data ObRunState
4646 | ObRunState_BackendStarted
4747 deriving (Eq, Show)
4848
49−cp :: FilePath
50−cp = $(staticWhich "cp")
49+cpPath :: FilePath
50+cpPath = $(staticWhich "cp")
5151
5252cabalPath :: FilePath
5353cabalPath = $(staticWhich "cabal")
@@ -73,6 +73,9 @@ lnPath = $(staticWhich "ln")
7373rmPath :: FilePath
7474rmPath = $(staticWhich "rm")
7575
76+rsyncPath :: FilePath
77+rsyncPath = $(staticWhich "rsync")
78+
7679gitUserConfig :: [Text]
7780gitUserConfig = ["-c", "user.name=Obelisk Selftest", "-c", "user.email=noreply@example.com"]
7881
@@ -84,12 +87,6 @@ commit msg = void $ run gitPath $ gitUserConfig <> [ "commit"
8487 , msg
8588 ]
8689
87−-- TODO replace Void and stop using loosely typed synchronous exceptions
88−runCli :: MonadIO m => CliT Void IO a -> m a
89−runCli f = liftIO $ do
90− c <- newCliConfig Notice False False (\case {})
91− CliApp.runCli c f
92−
9390tshow :: Show a => a -> Text
9491tshow = T.pack . show
9592
@@ -106,228 +103,255 @@ ob = "ob"
106103augmentWithVerbosity :: (String -> [Text] -> a) -> String -> Bool -> [Text] -> a
107104augmentWithVerbosity runner executable isVerbose args = runner executable $ (if isVerbose then ("-v" :) else id) args
108105
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+
109129main :: IO ()
110130main = do
111131 -- Note: you can pass hspec arguments as well, eg: `-m <pattern>`
112132 isVerbose <- elem "-v" <$> getArgs
113133 unless isVerbose $
114134 putStrLn "Tests may take longer to run if there are unbuilt derivations: use -v for verbose output"
115− let verbosity = bool silently verbosely isVerbose
116− nixBuild args = run nixBuildPath ("--no-out-link" : args)
117− obeliskImplDirtyReadOnly <- fromString <$> getCurrentDirectory
118− let runOb_ = augmentWithVerbosity run_ ob isVerbose
119− let runOb = augmentWithVerbosity run ob isVerbose
120− let testObRunInDir' = augmentWithVerbosity testObRunInDir ob isVerbose ["run"]
121− let testThunkPack' = augmentWithVerbosity testThunkPack ob isVerbose []
135+
136+ obeliskImplDirtyReadOnly <- getCurrentDirectory
137+ httpManager <- HTTP.newManager HTTP.defaultManagerSettings
138+
139+ withSystemTempDirectory "obelisk-repo-git-remote" $ \copyDir -> do
140+ copyForGitRemote isVerbose obeliskImplDirtyReadOnly copyDir
141+ main' isVerbose httpManager copyDir
142+
143+main' :: Bool -> HTTP.Manager -> FilePath -> IO ()
144+main' isVerbose httpManager obeliskRepoReadOnly = withInitCache $ \initCache -> hspec $ parallel $ do
122145 let
123− withObeliskImplClean f =
124− withSystemTempDirectory "obelisk-impl-clean" $ \(fromString -> obeliskImpl) -> do
125− void . shellyOb verbosity $ chdir obeliskImpl $ do
126− run_ gitPath ["clone", "file://" <> toTextIgnore obeliskImplDirtyReadOnly, toTextIgnore obeliskImpl]
127− f obeliskImpl
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 []
128309
129− withObeliskImplDirty f =
130− withSystemTempDirectory "obelisk-impl-copy" $ \(fromString -> obeliskImpl) -> do
310+ withObeliskImplClean f =
311+ withSystemTempDirectory "obelisk-impl-clean" $ \obeliskImpl -> do
131312 void . shellyOb verbosity $ chdir obeliskImpl $ do
132− user <- T.strip <$> run whoamiPath []
133− run_ cp ["-rT", "--no-preserve=mode", toTextIgnore obeliskImplDirtyReadOnly, toTextIgnore obeliskImpl]
134− run_ chownPath ["-R", user, toTextIgnore obeliskImpl]
135− run_ chmodPath ["-R", "g-rw,o-rw", toTextIgnore obeliskImpl]
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]
136316 f obeliskImpl
137317
138− withInitCache f obeliskImpl =
318+ withInitCache f =
139319 withSystemTempDirectory "init Cache λ" $ \initCache -> do
140320 -- Setup the ob init cache
141321 void . shellyOb verbosity $ chdir initCache $ do
142− runOb_ ["init", "--symlink", toTextIgnore obeliskImpl]
322+ runOb_ ["init", "--symlink", toTextIgnore obeliskRepoReadOnly]
143323 run_ gitPath ["init"]
144324
145325 f initCache
146326
147− withObeliskImplDirtyAndInitCache f =
148− withObeliskImplDirty $ \impl -> withInitCache (f impl) impl
327+ shelly_ = void . shellyOb verbosity
149328
150− httpManager <- HTTP.newManager HTTP.defaultManagerSettings
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]
151354
152− withObeliskImplDirtyAndInitCache $ \obeliskImplDirty initCache ->
153− hspec $ parallel $ do
154− let shelly_ = void . shellyOb verbosity
155−
156− defaultTmpDirName = "test λ"
157−
158− inTmp' :: FilePath -> (FilePath -> Sh a) -> IO ()
159− inTmp' dirname f = withTmp' dirname (chdir <*> f)
160− inTmp = inTmp' defaultTmpDirName
161−
162− withTmp' dirname f = shelly_ . withSystemTempDirectory dirname $ f . fromString
163− withTmp = withTmp' defaultTmpDirName
164−
165− inTmpObInit' dirname f = inTmp' dirname $ \dir -> do
166− run_ cp ["-rT", fromString initCache, toTextIgnore dir]
167− f dir
168− inTmpObInit = inTmpObInit' defaultTmpDirName
169−
170− -- To be used in tests that change the obelisk impl directory
171− inTmpObInitWithImplCopy f = inTmpObInit $ \dir ->
172− withObeliskImplClean $ \(fromString -> implClean) -> do
173− run_ rmPath [thunk]
174− run_ lnPath ["-s", implClean, thunk]
175− f dir
176−
177− assertRevEQ a b = liftIO . assertEqual "" "" =<< diff a b
178− assertRevNE a b = liftIO . assertBool "" . (/= "") =<< diff a b
179−
180− revParseHead = T.strip <$> run gitPath ["rev-parse", "HEAD"]
181−
182− commitAll = do
183− run_ gitPath ["add", "."]
184− commit "checkpoint"
185− revParseHead
186−
187− thunk = ".obelisk/impl"
188− update = runOb ["thunk", "update", thunk] >> commitAll
189− pack = runOb ["thunk", "pack", thunk] >> commitAll
190− unpack = runOb ["thunk", "unpack", thunk] >> commitAll
191−
192− diff a b = run gitPath ["diff", a, b]
193−
194− describe "ob init" $ parallel $ do
195− it "works with default impl" $ inTmp $ \_ -> runOb ["init"]
196− it "works with master branch impl" $ inTmp $ \_ -> runOb ["init", "--branch", "master"]
197− it "works with symlink" $ inTmp $ \_ -> runOb ["init", "--symlink", toTextIgnore obeliskImplDirty]
198− it "doesn't silently overwrite existing files" $ withSystemTempDirectory "ob-init λ" $ \dir -> do
199− let p force = (System.Process.proc ob $ "--no-handoff" : "-v" : "init" : ["--force"|force]) { cwd = Just dir }
200− (ExitSuccess, _, _) <- readCreateProcessWithExitCode (p False) ""
201− (ExitFailure _, _, _) <- readCreateProcessWithExitCode (p False) ""
202− (ExitSuccess, _, _) <- readCreateProcessWithExitCode (p True) ""
203− pure ()
204−
205− it "doesn't create anything when given an invalid impl" $ inTmp $ \tmp -> do
206− void $ errExit False $ runOb ["init", "--symlink", "/dev/null"]
207− ls tmp >>= liftIO . assertEqual "" []
208−
209− it "produces a valid route config" $ inTmpObInit $ \tmp -> liftIO $
210− withCurrentDirectory (T.unpack $ toTextIgnore tmp) $ do
211− configs <- getConfigs
212− return (either (const Nothing) Just $ getConfigRoute configs) `shouldNotReturn` Nothing
213−
214− -- These tests fail with "Could not find module 'Obelisk.Generated.Static'"
215− -- when not run by 'nix-build --attr selftest'
216− describe "ob run" $ {- NOT parallel $ -} do
217− it "works in root directory" $ inTmpObInit $ \_ -> testObRunInDir' Nothing httpManager
218− it "works in sub directory" $ inTmpObInit $ \_ -> testObRunInDir' (Just "frontend") httpManager
219−
220− describe "obelisk project" $ parallel $ do
221− it "can build obelisk command" $ inTmpObInit $ \_ -> nixBuild ["-A", "command" , toTextIgnore obeliskImplDirty]
222− it "can build obelisk skeleton" $ inTmpObInit $ \_ -> nixBuild ["-A", "skeleton", toTextIgnore obeliskImplDirty]
223− it "can build obelisk shell" $ inTmpObInit $ \_ -> nixBuild ["-A", "shell", toTextIgnore obeliskImplDirty]
224− it "can build everything" $ inTmpObInit $ \_ -> nixBuild [toTextIgnore obeliskImplDirty]
225−
226− describe "blank initialized project" $ parallel $ do
227−
228− it "can build ghc.backend" $ inTmpObInit $ \_ -> nixBuild ["-A", "ghc.backend"]
229− it "can build ghcjs.frontend" $ inTmpObInit $ \_ -> nixBuild ["-A", "ghcjs.frontend"]
230−
231− if System.Info.os == "darwin"
232− then it "can build ios" $ inTmpObInit $ \_ -> nixBuild ["-A", "ios.frontend"]
233− else it "can build android after accepting license" $ inTmpObInit $ \dir -> do
234− let defaultNixPath = dir </> ("default.nix" :: FilePath)
235− writefile defaultNixPath
236− =<< T.replace
237− "# config.android_sdk.accept_license = false;"
238− "config.android_sdk.accept_license = true;"
239− <$> readfile defaultNixPath
240− nixBuild ["-A", "android.frontend"]
241−
242− forM_ ["ghc", "ghcjs"] $ \compiler -> do
243− let
244− shellName = "shells." <> compiler
245− inShell cmd' = run_ "nix-shell" ["default.nix", "-A", fromString shellName, "--run", cmd']
246− it ("can enter " <> shellName) $ inTmpObInit $ \_ -> inShell "exit"
247− -- NOTE: We override the temporary directory name because cabal has a bug preventing new-build from working
248− -- in a path that has unicode characters.
249− it ("can build in " <> shellName) $ inTmpObInit' "test" $ \_ -> inShell $
250− T.pack cabalPath <> " --version; " <> T.pack cabalPath <> " new-build --" <> T.pack compiler <> " all"
251−
252− it "has idempotent thunk update" $ inTmpObInitWithImplCopy $ \_ -> do
253− _ <- pack
254− u <- update
255− uu <- update
256− assertRevEQ u uu
257−
258− describe "ob thunk pack/unpack" $ parallel $ do
259− it "has thunk pack and unpack inverses" $ inTmpObInitWithImplCopy $ \_ -> do
260−
261− _ <- pack
262− e <- commitAll
263− eu <- unpack
264− eup <- pack
265− eupu <- unpack
266− _ <- pack
267−
268− assertRevEQ e eup
269− assertRevEQ eu eupu
270− assertRevNE e eu
271−
272− it "unpacks the correct branch" $ withTmp $ \dir -> do
273− let branch = "master"
274− run_ gitPath ["clone", "https://github.com/reflex-frp/reflex.git", toTextIgnore dir, "--branch", branch]
275− runOb_ ["thunk", "pack", toTextIgnore dir]
276− runOb_ ["thunk", "unpack", toTextIgnore dir]
277− branch' <- chdir dir $ run gitPath ["rev-parse", "--abbrev-ref", "HEAD"]
278− liftIO $ assertEqual "" branch (T.strip branch')
279−
280− it "can pack and unpack plain git repos" $
281− shelly_ $ withSystemTempDirectory "git-repo" $ \dir -> do
282− let repo = toTextIgnore $ dir </> ("repo" :: String)
283− run_ gitPath ["clone", "https://github.com/haskell/process.git", repo]
284− origHash <- chdir (fromText repo) revParseHead
285−
286− runOb_ ["thunk", "pack", repo]
287− packedFiles <- Set.fromList <$> ls (fromText repo)
288− liftIO $ assertEqual "" packedFiles $ Set.fromList $ (repo </>) <$>
289− ["default.nix", "github.json", ".attr-cache" :: String]
290−
291− runOb_ ["thunk", "unpack", repo]
292− chdir (fromText repo) $ do
293− unpackHash <- revParseHead
294− assertRevEQ origHash unpackHash
295−
296− testThunkPack' $ fromText repo
297−
298− it "aborts thunk pack when there are uncommitted files" $ inTmpObInitWithImplCopy $ \dir -> do
299− testThunkPack' (dir </> thunk)
300−
301− describe "ob thunk update --branch" $ parallel $ do
302− it "can change a thunk to the latest version of a desired branch" $ withTmp $ \dir -> do
303− let branch1 = "master"
304− branch2 = "develop"
305− run_ gitPath ["clone", "https://github.com/reflex-frp/reflex.git", toTextIgnore dir, "--branch", branch1]
306− runOb_ ["thunk" , "pack", toTextIgnore dir]
307− runOb_ ["thunk", "update", toTextIgnore dir, "--branch", branch2]
308−
309− it "doesn't create anything when given an invalid branch" $ withTmp $ \dir -> do
310− let checkDir dir' = liftIO $ getDirectoryContents $ T.unpack $ toTextIgnore dir'
311− run_ gitPath ["clone", "https://github.com/reflex-frp/reflex.git", toTextIgnore dir, "--branch", "master"]
312− runOb_ ["thunk" , "pack", toTextIgnore dir]
313− startingContents <- checkDir dir
314− void $ errExit False $ runOb ["thunk", "update", toTextIgnore dir, "--branch", "dumble-palooza"]
315− checkDir dir >>= liftIO . assertEqual "" startingContents
316−
317− describe "ob hoogle" $ {- NOT parallel -} do
318− it "starts a hoogle server on the given port" $ inTmpObInit $ \_ -> do
319− [p0] <- liftIO $ getFreePorts 1
320− maskExitSuccess $ runHandle "ob" ["hoogle", "--port", T.pack (show p0)] $ \stdout -> fix $ \loop -> do
321− ln <- liftIO $ T.hGetLine stdout
322− echo ln
323− let search = "Server starting on port " <> T.pack (show p0)
324− case search `T.isInfixOf` ln of
325− False -> loop -- keep waiting
326− True -> do
327− let req uri = liftIO $ HTTP.parseRequest uri >>= flip HTTP.httpLbs httpManager
328− req ("http://127.0.0.1:" <> show p0) >>= \r -> case HTTP.responseStatus r == HTTP.ok200 of
329− False -> errorExit $ "Request to hoogle server failed: " <> T.pack (show r)
330− True -> exit 0
331355
332356maskExitSuccess :: Sh () -> Sh ()
333357maskExitSuccess = handle_sh (\case ExitSuccess -> pure (); e -> throw e)
nixpkgs-overlays/default.nixnew file+15
@@ -0,0 +1,15 @@
1+self: super:
2+
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);
13+
14+ obeliskExecutableConfig = self.callPackage ../lib/executable-config {};
15+}
skeleton/.gitignore+1
@@ -6,3 +6,4 @@ result-ios
66result-exe
77.attr-cache
88ghcid-output.txt
9+profile/