on master help sign in
Merge branch 'develop' into extra-local-packages
commitfe710240ce80d011f7da09aae067e9352c79d9e0 authorElliot Cameron <eacameron@gmail.com> dateTue 14 Jan 2020, 13:09 (7 years ago) parent246445b678d2d6
49 files changed+1247−641
.gitignore+1 .hlint.yaml+65 ChangeLog.md+30−4 FAQ.md+2−2 README.md+13−5 all-builds.nix+117 all-tests.nix+107 default.nix+40−43 dep/.gitignore+1 dep/ghcid/default.nix+7 dep/ghcid/github.json+7 dep/hnix/default.nix+7 dep/hnix/github.json+7 dep/reflex-platform/github.json+2−2 jobsets.nix−71 lib/asset/README.md+10−10 lib/cliapp/obelisk-cliapp.cabal+1 lib/cliapp/src/Obelisk/CliApp.hs+14−7 lib/cliapp/src/Obelisk/CliApp/Demo.hs−1 lib/cliapp/src/Obelisk/CliApp/Process.hs+86−38 lib/command/obelisk-command.cabal+2−1 lib/command/src/Obelisk/Command.hs+31−15 lib/command/src/Obelisk/Command/Deploy.hs+13−13 lib/command/src/Obelisk/Command/Nix.hs+1−2 lib/command/src/Obelisk/Command/Project.hs+77−33 lib/command/src/Obelisk/Command/Run.hs+90−71 lib/command/src/Obelisk/Command/Thunk.hs+180−102 lib/command/src/Obelisk/Command/Utils.hs+27−13 lib/command/src/Obelisk/Command/VmBuilder.hs−1 lib/executable-config/default.nix+1−1 lib/executable-config/lookup/default.nix−26 lib/executable-config/lookup/obelisk-executable-config-lookup.cabal+1−1 lib/executable-config/lookup/src/Obelisk/Configs.hs+1 lib/frontend/src/Obelisk/Frontend.hs+2−2 lib/route/src/Obelisk/Route.hs+48−1 lib/route/src/Obelisk/Route/Frontend.hs+36−9 lib/run/obelisk-run.cabal+7 lib/run/src/Obelisk/Run.hs+56−13 lib/selftest/obelisk-selftest.cabal+4−2 lib/selftest/src/Obelisk/SelfTest.hs+115−57 lib/tabulation/src/Data/Tabulation.hs+1−1 release.nix+8−86 scripts/ghcid-this-lib+1−1 skeleton/.gitignore+1 skeleton/common/common.cabal−3 skeleton/common/src/Common/Route.hs+1 skeleton/frontend/frontend.cabal+1 skeleton/frontend/src/Frontend.hs+22−4 skeleton/static/main.css+3
.gitignore+1
@@ -2,6 +2,7 @@
22**/.cabal-sandbox
33**/cabal.sandbox.config
44result
5+result-*
56*.o
67*.hi
78TAGS
.hlint.yamlnew file+65
@@ -0,0 +1,65 @@
1+# HLint configuration file
2+# https://github.com/ndmitchell/hlint
3+##########################
4+
5+# This file contains a template configuration file, which is typically
6+# placed as .hlint.yaml in the root of your project
7+
8+
9+# Specify additional command line arguments
10+#
11+# - arguments: [--color, --cpp-simple, -XQuasiQuotes]
12+
13+
14+# Control which extensions/flags/modules/functions can be used
15+#
16+# - extensions:
17+# - default: false # all extension are banned by default
18+# - name: [PatternGuards, ViewPatterns] # only these listed extensions can be used
19+# - {name: CPP, within: CrossPlatform} # CPP can only be used in a given module
20+#
21+# - flags:
22+# - {name: -w, within: []} # -w is allowed nowhere
23+#
24+# - modules:
25+# - {name: [Data.Set, Data.HashSet], as: Set} # if you import Data.Set qualified, it must be as 'Set'
26+# - {name: Control.Arrow, within: []} # Certain modules are banned entirely
27+#
28+# - functions:
29+# - {name: unsafePerformIO, within: []} # unsafePerformIO can only appear in no modules
30+
31+
32+# Add custom hints for this project
33+#
34+# Will suggest replacing "wibbleMany [myvar]" with "wibbleOne myvar"
35+# - error: {lhs: "wibbleMany [x]", rhs: wibbleOne x}
36+
37+
38+# Turn on hints that are off by default
39+#
40+# Ban "module X(module X) where", to require a real export list
41+# - warn: {name: Use explicit module export list}
42+#
43+# Replace a $ b $ c with a . b $ c
44+# - group: {name: dollar, enabled: true}
45+#
46+# Generalise map to fmap, ++ to <>
47+# - group: {name: generalise, enabled: true}
48+
49+
50+# Ignore some builtin hints
51+- ignore: {name: Redundant do}
52+- ignore: {name: Redundant flip}
53+- ignore: {name: Use <$>}
54+- ignore: {name: Use camelCase}
55+- ignore: {name: Use if}
56+- ignore: {name: Use unless}
57+# - ignore: {name: Use const, within: SpecialModule} # Only within certain modules
58+
59+
60+# Define some custom infix operators
61+# - fixity: infixr 3 ~^#^~
62+
63+
64+# To generate a suitable file for HLint do:
65+# $ hlint --default > .hlint.yaml
ChangeLog.md+30−4
@@ -4,12 +4,38 @@ This project's release branch is `master`. This log is written from the perspect
44
55## Unreleased
66
7−* Generalised pathSegmentEncoder, added pathFieldEncoder.
8−* Added some Prisms to the encoder library for manipulating DSums (perhaps they should get moved to dependent-sum before release?)
9−* Add `ob doc` command, which lists paths to haddock documentation for specified packages
10−* Bump reflex-platform so that obelisk now uses GHC 8.6.5 and the nixos-19.03 nixpkgs set
117* Add local packages from the Nix `project { packages }` option to the GHCi config, so that they are auto-reloaded with `ob run`.
128
9+## v0.4.0.0 - 2020-01-10
10+
11+* Bump reflex-platform which, notably, bumps nixpkgs to 19.09. ([#585](https://github.com/obsidiansystems/obelisk/pull/585))
12+* Add new thunk loader for Git repositories that supports `file://` Git remotes and supports private repositories via `builtins.fetchGit` for private repositories (when the `git.json` file specifies `"private": true`). ([#594](https://github.com/obsidiansystems/obelisk/pull/594))
13+* Add a new thunk loader for GitHub repositories that uses `builtins.fetchTarball` for public repositories to increase loader performance and uses `fetchFromGitHub` for private repositories (when the `github.json` file specifies `"private": true`). Note that `fetchFromGitHub` requires some Nix configuration for the Nix builder to access the repository. If `ob thunk pack` fails in this case, use `-v` to see Nix's helpful message. ([#594](https://github.com/obsidiansystems/obelisk/pull/594))
14+* Add `--public`/`--private` options to `ob thunk pack` to specify if a repository should be treated as a public or private. ([#594](https://github.com/obsidiansystems/obelisk/pull/594))
15+* Improve error messaging when a dependency doesn't have the expected `.cabal` or `package.yaml` file. ([#597](https://github.com/obsidiansystems/obelisk/pull/597))
16+* Improve the skeleton in small ways. ([#593](https://github.com/obsidiansystems/obelisk/pull/593), [#589](https://github.com/obsidiansystems/obelisk/pull/589))
17+* Fix `ob` commands to again support running from any subdirectory of an obelisk project ([#591](https://github.com/obsidiansystems/obelisk/pull/591))
18+* Add `reflex-platform-func` argument to Obelisk's `default.nix`. It defaults to it's prior behavior of using the reflex-platform in in `dep`. ([#612](https://github.com/obsidiansystems/obelisk/pull/612))
19+
20+## v0.3.0.0 - 2019-12-20
21+
22+* Generalised `pathSegmentEncoder`, added `pathFieldEncoder`.
23+* Added some `Prism`s to the encoder library for manipulating `DSum`s.
24+* Add `ob doc` command, which lists paths to haddock documentation for specified packages.
25+* Bump reflex-platform so that obelisk now uses GHC 8.6.5 and the nixos-19.03 nixpkgs set.
26+* Add support in `obelisk-route` for single parameters in URL paths.
27+* Bump reflex-platform so that obelisk now uses reflex-dom 0.5.2.0.
28+* Use a `--pure` nix shell in `ob run` for parity with `ob repl` and more resilience against "works on my machine".
29+* Pin usages of `<nixpkgs>` in obelisk thunks, etc. to the nixpkgs used by the project's obelisk impl.
30+* Backport ACMEv2 support in obelisk server to regain LetsEncrypt account creation.
31+* Enable HTTPS in `ob run`.
32+* `ob run` now handles `ghci` errors better, and includes a custom `ghcid`
33+ version. As a result, you no longer need to have ghcid installed to
34+ use `ob run`, as we provide one for you.
35+* `ob` commands now complain less on systems with umasks other than `0022`.
36+* Ignore package environment files in `ob run` and `ob repl`.
37+* Add `Obelisk.Route.Frontend.routeLinkDynAttr`.
38+
1339## v0.2.0.0 - 2019-8-17
1440
1541* Configs become ByteStrings.
FAQ.md+2−2
@@ -81,7 +81,7 @@ project's dependencies and,
8181Assuming we are in a project created with `ob init`, `ob run` calls (see
8282`lib/command/src/Obelisk/Command.hs`):
8383
84− nix-shell -A shells.ghc --run 'ob --no-handoff internal run-static-io <real-run-function>'
84+ nix-shell --pure -A shells.ghc --run 'ob --no-handoff internal run-static-io <real-run-function>'
8585
8686where
8787
@@ -109,7 +109,7 @@ It is defined in `lib/run/src/Obelisk/Run.hs`:
109109 * or to `serveDefaultObeliskApp` (`lib/backend/src/Obelisk/Backend.hs`) to
110110 serve static assets.
111111* Starts `runWidget`
112− which itself runs
112+ which itself runs
113113 [runSettingsSocket](https://hackage.haskell.org/package/warp-3.2.26/docs/Network-Wai-Handler-Warp.html#v:runSettingsSocket)
114114 (the *“TCP listen loop”*):
115115 * it binds to TCP socket, and
README.md+13−5
@@ -7,6 +7,7 @@ Obelisk provides an easy way to develop and deploy your [Reflex](https://github.
77 - [Hoogle](#hoogle)
88 - [Adding Packages](#adding-packages)
99 - [Adding Package Overrides](#adding-package-overrides)
10+ - [Running over https](#running-over-https)
1011- [Deploying](#deploying)
1112 - [Locally](#locally)
1213 - [EC2](#ec2)
@@ -68,8 +69,8 @@ nix-env -f /path/to/obelisk -iA command
6869Note that `ob` will defer to the version found in your project's `.obelisk/impl` directory. To update that version specifically:
6970
7071```shell
71−ob thunk unpack ./obelisk/impl
72−cd ./obelisk/impl
72+ob thunk unpack ./.obelisk/impl
73+cd ./.obelisk/impl
7374# apply your changes
7475```
7576
@@ -105,7 +106,8 @@ Obelisk leverages ghcid to provide a live-reloading server that handles both fro
105106ob run
106107```
107108
108−Now go to http://localhost:8000 (or the address/port specified in `config/common/route`) to access your app.
109+Now, with an appropriate browser, go to http://localhost:8000 (or the address/port specified in `config/common/route`) to access your app.
110+Firefox will not be able to properly run the development website due to [issue 460](https://github.com/obsidiansystems/obelisk/issues/460). Fortunately, this problem does not occur on a fully built website.
109111
110112Every time you change the Haskell source files in frontend, common or backend, `ob run` will automatically recompile the modified files and reload the server. Furthermore, it will display on screen compilation errors and warnings if any.
111113
@@ -165,6 +167,12 @@ project ./. ({ pkgs, ... }: {
165167# ...
166168```
167169
170+### Running over https
171+
172+To run your app locally over https, update the protocol in `config/common/route` to `https`, and then use `ob run` as normal.
173+
174+Since Obelisk generates a self-signed certificate for running https, the browser will issue a warning about using an invalid certificate. On Chrome, you can go to `chrome://flags/#allow-insecure-localhost` to enable invalid certificates for localhost.
175+
168176## Deploying
169177
170178### Locally
@@ -343,9 +351,9 @@ This command will accomplish the following:
3433511. Build a Signed Android apk for your application
3443521. Deploy the Signed apk to your connected Android device
345353
346−In the event that you change your key or keystore password, you will have to update your credentials within the JSON object found in `android_keytool_config.json`
354+In the event that you change your key or keystore password, you will have to update your credentials within the JSON object found in `android_keytool_config.json`.
347355
348−Additional documentation on java key stores can be found [here] (https://docs.oracle.com/javase/8/docs/technotes/tools/unix/keytool.html)
356+Additional documentation on Java key stores can be found [here](https://docs.oracle.com/javase/8/docs/technotes/tools/unix/keytool.html).
349357
350358This should copy over and install the application on your device (if you see a "*signatures do not match*" error, simply uninstall the previous app from the device before retrying the deploy). The name of the installed application will be what you have specified for `android.displayName` in the `default.nix`.
351359
all-builds.nixnew file+117
@@ -0,0 +1,117 @@
1+{ self-args ? {
2+ config.android_sdk.accept_license = true;
3+ iosSdkVersion = "10.2";
4+ }
5+, local-self ? import ./. self-args
6+, supportedSystems ? [
7+ "x86_64-linux"
8+ "x86_64-darwin"
9+ ]
10+}:
11+
12+let
13+ inherit (local-self.nixpkgs) lib runCommand nix;
14+ cacheBuildSystems = supportedSystems;
15+
16+ obeliskPackagesCommon = [
17+ "obelisk-frontend"
18+ "obelisk-route"
19+ "obelisk-executable-config-lookup"
20+ ];
21+
22+ obeliskPackagesBackend = obeliskPackagesCommon ++ [
23+ "obelisk-asset-manifest"
24+ "obelisk-asset-serve-snap"
25+ "obelisk-backend"
26+ "obelisk-cliapp"
27+ "obelisk-command"
28+ "obelisk-executable-config-inject"
29+ "obelisk-frontend"
30+ "obelisk-run"
31+ "obelisk-route"
32+ "obelisk-selftest"
33+ "obelisk-snap-extras"
34+ ];
35+
36+ pnameToAttrs = pkgsSet: pnames:
37+ lib.listToAttrs (map
38+ (name: { inherit name; value = pkgsSet.${name}; })
39+ pnames);
40+
41+ concatDepends = let
42+ extractDeps = x: (x.override {
43+ mkDerivation = drv: {
44+ out = builtins.concatLists [
45+ (drv.buildDepends or [])
46+ (drv.libraryHaskellDepends or [])
47+ (drv.executableHaskellDepends or [])
48+ ];
49+ };
50+ }).out;
51+ in pkgAttrs: builtins.concatLists (map extractDeps (builtins.attrValues pkgAttrs));
52+
53+ perPlatform = lib.genAttrs cacheBuildSystems (system: let
54+ reflex-platform = import ./dep/reflex-platform { inherit system; };
55+ perProfiling = profiling: let
56+ obelisk = import ./. (self-args // { inherit system profiling; });
57+ ghc = pnameToAttrs
58+ obelisk.haskellPackageSets.ghc
59+ obeliskPackagesBackend;
60+ ghcjs = pnameToAttrs
61+ obelisk.haskellPackageSets.ghcjs
62+ obeliskPackagesCommon;
63+ cachePackages = builtins.concatLists [
64+ (builtins.attrValues ghc)
65+ (lib.optionals (!profiling) (builtins.attrValues ghcjs))
66+ (concatDepends ghc)
67+ (lib.optionals (!profiling) (concatDepends ghcjs))
68+ (lib.optional reflex-platform.androidSupport androidSkeleton)
69+ (lib.optional reflex-platform.iosSupport iosSkeleton)
70+ (lib.optionals (!profiling) [
71+ command
72+ serverSkeletonExe
73+ serverSkeletonShell
74+ ])
75+ ];
76+ command = obelisk.command;
77+ skeleton = import ./skeleton { inherit obelisk; };
78+ serverSkeletonExe = skeleton.exe;
79+ # TODO fix nixpkgs so it doesn't try to run the result of haskell shells as setup hooks.
80+ serverSkeletonShell = local-self.nixpkgs.runCommand "shell-safe-for-dep" {} ''
81+ touch "$out"
82+ echo "return" >> "$out"
83+ cat "${skeleton.shells.ghc}" >> "$out"
84+ '';
85+ androidSkeleton = (import ./skeleton { inherit obelisk; }).android.frontend;
86+ iosSkeleton = (import ./skeleton { inherit obelisk; }).ios.frontend;
87+ nameSuffix = if profiling then "profiled" else "unprofiled";
88+ in {
89+ inherit
90+ ghc
91+ ;
92+ cache = reflex-platform.pinBuildInputs "obelisk-${system}-${nameSuffix}" cachePackages;
93+ } // lib.optionalAttrs (!profiling) {
94+ inherit
95+ command
96+ ghcjs
97+ serverSkeletonExe
98+ serverSkeletonShell
99+ ;
100+ } // lib.optionalAttrs reflex-platform.androidSupport {
101+ inherit androidSkeleton;
102+ } // lib.optionalAttrs reflex-platform.iosSupport {
103+ inherit iosSkeleton;
104+ };
105+ profiled = perProfiling true;
106+ unprofiled = perProfiling false;
107+ cachePackages = map (p: p.cache) [ profiled unprofiled ];
108+ in {
109+ inherit profiled unprofiled;
110+ cache = reflex-platform.pinBuildInputs "obelisk-${system}" cachePackages;
111+ });
112+
113+ metaCache = local-self.reflex-platform.pinBuildInputs
114+ "obelisk-everywhere"
115+ (map (a: a.cache) (builtins.attrValues perPlatform));
116+
117+in perPlatform // { inherit metaCache; }
all-tests.nixnew file+107
@@ -0,0 +1,107 @@
1+{
2+ supportedSystems ? [ builtins.currentSystem ]
3+}:
4+let
5+ nginxRoot = "/run/nginx";
6+ obelisk = import ./default.nix {};
7+ # Get NixOS a pre-release 20.03 that contains the python based tests and recursive nix
8+ pkgs = import (builtins.fetchTarball https://github.com/nixos/nixpkgs/archive/3de5266.tar.gz) {};
9+ sshKeys = import (pkgs.path + /nixos/tests/ssh-keys.nix) pkgs;
10+ make-test = import (pkgs.path + /nixos/tests/make-test-python.nix);
11+ obelisk-everywhere = (import ./all-builds.nix { inherit supportedSystems; }).x86_64-linux.cache;
12+ snakeOilPrivateKey = sshKeys.snakeOilPrivateKey.text;
13+ snakeOilPublicKey = sshKeys.snakeOilPublicKey;
14+in
15+ make-test ({...}: {
16+ name = "obelisk";
17+ nodes = {
18+ githost = {
19+ networking.firewall.allowedTCPPorts = [ 22 80 ];
20+ services.openssh = {
21+ enable = true;
22+ };
23+ environment.systemPackages = [
24+ pkgs.git
25+ ];
26+ users.users.root.openssh.authorizedKeys.keys = [
27+ snakeOilPublicKey
28+ ];
29+ };
30+
31+ client = {
32+ imports = [
33+ (pkgs.path + /nixos/modules/installer/cd-dvd/channel.nix)
34+ ];
35+ nix.useSandbox = false;
36+ nix.binaryCaches = [];
37+ environment.systemPackages = [
38+ obelisk.command
39+ obelisk.shell
40+ obelisk-everywhere
41+ pkgs.git
42+ ];
43+ };
44+ };
45+
46+ testScript =
47+ let
48+ privateKeyFile = pkgs.writeText "id_rsa" ''${snakeOilPrivateKey}'';
49+ sshConfigFile = pkgs.writeText "ssh_config" ''
50+ Host *
51+ StrictHostKeyChecking no
52+ UserKnownHostsFile=/dev/null
53+ ConnectionAttempts=1
54+ ConnectTimeout=1
55+ IdentityFile=~/.ssh/id_rsa
56+ User=root
57+ '';
58+ in ''
59+ start_all()
60+ githost.wait_for_open_port("22")
61+
62+ with subtest("test obelisk is installed"):
63+ client.succeed("ob --help")
64+
65+ with subtest("test the client can access the server via ssh"):
66+ client.succeed("mkdir -p ~/.ssh/")
67+ client.succeed(
68+ "cp ${privateKeyFile} ~/.ssh/id_rsa"
69+ )
70+ client.succeed("chmod 600 ~/.ssh/id_rsa")
71+ client.wait_until_succeeds(
72+ "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa githost true"
73+ )
74+ client.succeed(
75+ "cp ${sshConfigFile} ~/.ssh/config"
76+ )
77+ client.wait_until_succeeds("ssh githost true")
78+
79+ with subtest("test a remote bare repo can be started"):
80+ githost.succeed("mkdir -p ~/myorg/myapp.git")
81+ githost.succeed("cd ~/myorg/myapp.git && git init --bare")
82+
83+ with subtest("test a git project can be configured with a remote using ssh"):
84+ client.succeed("mkdir -p ~/code/myapp")
85+ client.succeed("cd ~/code/myapp && git init")
86+ client.succeed("cd ~/code/myapp && touch README")
87+ client.succeed("cd ~/code/myapp && git add .")
88+ client.succeed('git config --global user.email "you@example.com"')
89+ client.succeed('git config --global user.name "Your Name"')
90+ client.succeed('cd ~/code/myapp && git commit -m "Initial"')
91+ client.succeed(
92+ "cd ~/code/myapp && git remote add origin root@githost:/root/myorg/myapp.git"
93+ )
94+
95+ with subtest("test pushing code to the remote"):
96+ client.succeed("cd ~/code/myapp && git push -u origin master")
97+ client.succeed("cd ~/code/myapp && git status")
98+
99+ with subtest("test obelisk can pack"):
100+ client.succeed("ob thunk pack ~/code/myapp")
101+ client.succeed("grep -qF 'git' ~/code/myapp/default.nix")
102+ client.succeed("grep -qF 'myorg' ~/code/myapp/git.json")
103+
104+ with subtest("test obelisk can detect private repos"):
105+ client.succeed("""grep -qF '"private": false' ~/code/myapp/git.json""")
106+ '';
107+ }) {}
default.nix+40−43
@@ -2,6 +2,7 @@
22, profiling ? false
33, iosSdkVersion ? "10.2"
44, config ? {}
5+, reflex-platform-func ? import ./dep/reflex-platform
56}:
67let
78 cleanSource = builtins.filterSource (name: _: let baseName = builtins.baseNameOf name; in !(
@@ -16,7 +17,7 @@ let
1617 openssh
1718 ];
1819
19− getReflexPlatform = sys: import ./dep/reflex-platform {
20+ getReflexPlatform = sys: reflex-platform-func {
2021 inherit iosSdkVersion config;
2122 system = sys;
2223 enableLibraryProfiling = profiling;
@@ -34,11 +35,10 @@ let
3435 pkgs = self.callPackage ({ pkgs }: pkgs) {};
3536 haskellLib = pkgs.haskell.lib;
3637 in {
37− hnix = haskellLib.overrideCabal (self.callHackage "hnix" "0.6.1" { these = self.these_0_8; }) (_: {
38− jailbreak = true;
39− doCheck = false;
40− });
38+ hnix = haskellLib.dontCheck (haskellLib.doJailbreak (self.callCabal2nix "hnix" (hackGet dep/hnix) {}));
4139 hnix-store-core = self.callHackage "hnix-store-core" "0.1.0.0" {};
40+
41+ ghcid = self.callCabal2nix "ghcid" (hackGet ./dep/ghcid) {};
4242 })
4343
4444 pkgs.obeliskExecutableConfig.haskellOverlay
@@ -46,18 +46,33 @@ let
4646 # Add obelisk packages
4747 (self: super: let
4848 pkgs = self.callPackage ({ pkgs }: pkgs) {};
49+ onLinux = pkg: f: if pkgs.stdenv.isLinux then f pkg else pkg;
4950 in {
51+ shelly = self.callHackage "shelly" "1.9.0" {};
52+
5053 obelisk-executable-config-inject = pkgs.obeliskExecutableConfig.platforms.web.inject self;
5154
52− obelisk-asset-manifest = self.callCabal2nix "obelisk-asset-manifest" (hackGet ./lib/asset + "/manifest") {};
53− obelisk-asset-serve-snap = self.callCabal2nix "obelisk-asset-serve-snap" (hackGet ./lib/asset + "/serve-snap") {};
55+ obelisk-asset-manifest = self.callCabal2nix "obelisk-asset-manifest" (cleanSource ./lib/asset/manifest) {};
56+ obelisk-asset-serve-snap = self.callCabal2nix "obelisk-asset-serve-snap" (cleanSource ./lib/asset/serve-snap) {};
5457 obelisk-backend = self.callCabal2nix "obelisk-backend" (cleanSource ./lib/backend) {};
5558 obelisk-cliapp = self.callCabal2nix "obelisk-cliapp" (cleanSource ./lib/cliapp) {};
56− obelisk-command = self.callCabal2nix "obelisk-command" (cleanSource ./lib/command) {};
59+ obelisk-command = haskellLib.overrideCabal (self.callCabal2nix "obelisk-command" (cleanSource ./lib/command) {}) {
60+ librarySystemDepends = [
61+ pkgs.nix
62+ (haskellLib.justStaticExecutables self.ghcid)
63+ ];
64+ };
5765 obelisk-frontend = self.callCabal2nix "obelisk-frontend" (cleanSource ./lib/frontend) {};
58− obelisk-run = self.callCabal2nix "obelisk-run" (cleanSource ./lib/run) {};
66+ obelisk-run = onLinux (self.callCabal2nix "obelisk-run" (cleanSource ./lib/run) {}) (pkg:
67+ haskellLib.overrideCabal pkg (drv: { librarySystemDepends = [ pkgs.iproute ]; })
68+ );
5969 obelisk-route = self.callCabal2nix "obelisk-route" (cleanSource ./lib/route) {};
60− obelisk-selftest = self.callCabal2nix "obelisk-selftest" (cleanSource ./lib/selftest) {};
70+ obelisk-selftest = haskellLib.overrideCabal (self.callCabal2nix "obelisk-selftest" (cleanSource ./lib/selftest) {}) {
71+ librarySystemDepends = [
72+ pkgs.cabal-install
73+ pkgs.coreutils
74+ ];
75+ };
6176 obelisk-snap-extras = self.callCabal2nix "obelisk-snap-extras" (cleanSource ./lib/snap-extras) {};
6277 tabulation = self.callCabal2nix "tabulation" (cleanSource ./lib/tabulation) {};
6378 })
@@ -65,31 +80,11 @@ let
6580 (self: super: let
6681 pkgs = self.callPackage ({ pkgs }: pkgs) {};
6782 haskellLib = pkgs.haskell.lib;
68− #TODO: Upstream
69− # Modify a Haskell package to add completion scripts for the given
70− # executable produced by it. These completion scripts will be picked up
71− # automatically if the resulting derivation is installed, e.g. by
72− # `nix-env -i`.
73− addOptparseApplicativeCompletionScripts = exeName: pkg: haskellLib.overrideCabal pkg (drv: {
74− postInstall = (drv.postInstall or "") + ''
75− BASH_COMP_DIR="$out/share/bash-completion/completions"
76− mkdir -p "$BASH_COMP_DIR"
77− "$out/bin/${exeName}" --bash-completion-script "$out/bin/${exeName}" >"$BASH_COMP_DIR/ob"
78−
79− ZSH_COMP_DIR="$out/share/zsh/vendor-completions"
80− mkdir -p "$ZSH_COMP_DIR"
81− "$out/bin/${exeName}" --zsh-completion-script "$out/bin/${exeName}" >"$ZSH_COMP_DIR/_ob"
82−
83− FISH_COMP_DIR="$out/share/fish/vendor_completions.d"
84− mkdir -p "$FISH_COMP_DIR"
85− "$out/bin/${exeName}" --fish-completion-script "$out/bin/${exeName}" >"$FISH_COMP_DIR/ob.fish"
86− '';
87− });
8883 in {
8984 # Dynamic linking with split objects dramatically increases startup time (about
9085 # 0.5 seconds on a decent machine with SSD), so we do `justStaticExecutables`.
9186 obelisk-command = haskellLib.overrideCabal
92− (addOptparseApplicativeCompletionScripts "ob"
87+ (haskellLib.generateOptparseApplicativeCompletion "ob"
9388 (haskellLib.justStaticExecutables super.obelisk-command))
9489 (drv: {
9590 buildTools = (drv.buildTools or []) ++ [ pkgs.buildPackages.makeWrapper ];
@@ -126,7 +121,7 @@ in rec {
126121 pathGit = ./.; # Used in CI by the migration graph hash algorithm to correctly ignore files.
127122 path = reflex-platform.filterGit ./.;
128123 obelisk = ghcObelisk;
129− obeliskEnvs = ghcObeliskEnvs;
124+ obeliskEnvs = pkgs.lib.filterAttrs (k: _: pkgs.lib.strings.hasPrefix "obelisk-" k) ghcObeliskEnvs;
130125 command = ghcObelisk.obelisk-command;
131126 shell = pinBuildInputs "obelisk-shell" ([command] ++ commandRuntimeDeps pkgs);
132127
@@ -135,10 +130,10 @@ in rec {
135130 set -euo pipefail
136131
137132 PATH="${command}/bin:$PATH"
138− export OBELISK_IMPL="${hackGet ./.}"
133+ export OBELISK_IMPL="$(mktemp -d)"
134+ git clone file://${pathGit} $OBELISK_IMPL
139135 "${ghcObelisk.obelisk-selftest}/bin/obelisk-selftest" +RTS -N -RTS "$@"
140136 '';
141− #TODO: Why can't I build ./skeleton directly as a derivation? `nix-build -E ./.` doesn't work
142137 skeleton = pkgs.runCommand "skeleton" {
143138 dir = builtins.filterSource (path: type: builtins.trace path (baseNameOf path != ".obelisk")) ./skeleton;
144139 } ''
@@ -160,6 +155,7 @@ in rec {
160155 compressedJs = frontend: optimizationLevel: pkgs.runCommand "compressedJs" {} ''
161156 mkdir $out
162157 cd $out
158+ # TODO profiling + static shouldn't break and need an ad-hoc workaround like that
163159 ln -s "${haskellLib.justStaticExecutables frontend}/bin/frontend.jsexe/all.js" all.unminified.js
164160 ${if optimizationLevel == null then ''
165161 ln -s all.unminified.js all.js
@@ -242,7 +238,7 @@ in rec {
242238 pkgs.runCommand "serverExe" {} ''
243239 mkdir $out
244240 set -eux
245− ln -s "${haskellLib.justStaticExecutables backend}"/bin/* $out/
241+ ln -s "${if profiling then backend else haskellLib.justStaticExecutables backend}"/bin/* $out/
246242 ln -s "${mkAssets assets}" $out/static.assets
247243 ln -s ${mkAssets (compressedJs frontend optimizationLevel)} $out/frontend.jsexe.assets
248244 echo ${version} > $out/version
@@ -340,18 +336,19 @@ in rec {
340336 passthru = { inherit android ios packages overrides tools shellToolOverrides withHoogle staticFiles staticFilesImpure __closureCompilerOptimizationLevel processedStatic __iosWithConfig __androidWithConfig; };
341337 };
342338 in mkProject (projectDefinition args));
343− serverOn = sys: version: serverExe
344− (projectOut sys).ghc.backend
345− (projectOut system).ghcjs.frontend
346− (projectOut sys).passthru.staticFiles
347− (projectOut sys).passthru.__closureCompilerOptimizationLevel
339+ mainProjectOut = projectOut system;
340+ serverOn = projectInst: version: serverExe
341+ projectInst.ghc.backend
342+ mainProjectOut.ghcjs.frontend
343+ projectInst.passthru.staticFiles
344+ projectInst.passthru.__closureCompilerOptimizationLevel
348345 version;
349− linuxExe = serverOn "x86_64-linux";
346+ linuxExe = serverOn (projectOut "x86_64-linux");
350347 dummyVersion = "Version number is only available for deployments";
351− in projectOut system // {
348+ in mainProjectOut // {
352349 linuxExeConfigurable = linuxExe;
353350 linuxExe = linuxExe dummyVersion;
354− exe = serverOn system dummyVersion;
351+ exe = serverOn mainProjectOut dummyVersion;
355352 server = args@{ hostName, adminEmail, routeHost, enableHttps, version }:
356353 server (args // { exe = linuxExe version; });
357354 obelisk = import (base + "/.obelisk/impl") {};
dep/.gitignore+1
@@ -1,4 +1,5 @@
11*
2+!*/
23!.gitignore
34!*/*.json
45!*/default.nix
dep/ghcid/default.nixnew file+7
@@ -0,0 +1,7 @@
1+# DO NOT HAND-EDIT THIS FILE
2+import ((import <nixpkgs> {}).fetchFromGitHub (
3+ let json = builtins.fromJSON (builtins.readFile ./github.json);
4+ in { inherit (json) owner repo rev sha256;
5+ private = json.private or false;
6+ }
7+))
dep/ghcid/github.jsonnew file+7
@@ -0,0 +1,7 @@
1+{
2+ "owner": "ndmitchell",
3+ "repo": "ghcid",
4+ "branch": "master",
5+ "rev": "f572318f32b1617f6054248e5888af68222f8e50",
6+ "sha256": "1icg3r70lg2kmd9gdc024ih1n9nrja98yav74z9nvykqygvv5w0n"
7+}
dep/hnix/default.nixnew file+7
@@ -0,0 +1,7 @@
1+# DO NOT HAND-EDIT THIS FILE
2+import ((import <nixpkgs> {}).fetchFromGitHub (
3+ let json = builtins.fromJSON (builtins.readFile ./github.json);
4+ in { inherit (json) owner repo rev sha256;
5+ private = json.private or false;
6+ }
7+))
dep/hnix/github.jsonnew file+7
@@ -0,0 +1,7 @@
1+{
2+ "owner": "haskell-nix",
3+ "repo": "hnix",
4+ "branch": "master",
5+ "rev": "6c9c7c310c54372b3db0fdf5a0137b395cde1bdb",
6+ "sha256": "1i5903b7lxqn2s3jarb14h6wdq8bxiik1hp0xy43w5w1hgvvq0g5"
7+}
dep/reflex-platform/github.json+2−2
@@ -2,6 +2,6 @@
22 "owner": "reflex-frp",
33 "repo": "reflex-platform",
44 "branch": "develop",
5− "rev": "9e306f72ed0dbcdccce30a4ba0eb37aa03cf91e3",
6− "sha256": "1crlwfw6zsx2izwsd57i5651p5pfyrpkfjwp6fc1yd4q4d4n7g2m"
5+ "rev": "90351bdb2c99d416ea95fd5b8b474e8f9fab2942",
6+ "sha256": "0bbzz4ckdx0ilhhpd3drfbsc0zdb2rnhw2m3nls9villbs3lz3lh"
77}
jobsets.nixdeleted−71
@@ -1,71 +0,0 @@
1−{ prs }:
2−
3−let
4− self = import ./. {};
5− pkgs = self.nixpkgs;
6− mkFetchGithub = value: {
7− inherit value;
8− type = "git";
9− emailresponsible = false;
10− };
11−in
12−with pkgs.lib;
13−let
14− defaults = jobs: {
15− inherit (jobs) description;
16− enabled = 1;
17− hidden = false;
18− keepnr = 10;
19− schedulingshares = 100;
20− checkinterval = 120;
21− enableemail = false;
22− emailoverride = "";
23− nixexprinput = "obelisk";
24− nixexprpath = "release.nix";
25− inputs = jobs.inputs // {
26− nixpkgs = {
27− type = "git";
28− value = "https://github.com/NixOS/nixpkgs-channels nixos-unstable";
29− emailresponsible = false;
30− };
31− };
32− self-args = {
33− type = "nix";
34− value = "{ config.android_sdk.accept_license = true; }";
35− emailresponsible = false;
36− };
37− };
38− branchJobset = branch: defaults {
39− description = "obelisk-${branch}";
40− inputs = {
41− obelisk = {
42− value = "https://github.com/obsidiansystems/obelisk ${branch}";
43− type = "git";
44− emailresponsible = false;
45− };
46− };
47− };
48− makePr = num: info: {
49− name = "obelisk-pr-${num}";
50− value = defaults {
51− description = "#${num}: ${info.title}";
52− inputs = {
53− obelisk = {
54− #NOTE: This should really use "pull/${num}/merge"; however, GitHub's
55− #status checks only operate on PR heads. This creates a race
56− #condition, which can currently only be solved by requiring PRs to be
57− #up to date before they're merged. See
58− #https://github.com/isaacs/github/issues/1002
59− value = "https://github.com/obsidiansystems/obelisk pull/${num}/head";
60− type = "git";
61− emailresponsible = false;
62− };
63− };
64− };
65− };
66− processedPrs = mapAttrs' makePr (builtins.fromJSON (builtins.readFile prs));
67− jobsetsAttrs = processedPrs //
68− genAttrs ["master"] branchJobset;
69−in {
70− jobsets = pkgs.writeText "spec.json" (builtins.toJSON jobsetsAttrs);
71−}
lib/asset/README.md+10−10
@@ -1,7 +1,7 @@
11## obelisk-asset
22
3−The 'assets.nix' file contains nix expressions that are a mutually
4−recursive set of attributes used to create static asset directories
3+The 'assets.nix' file contains nix expressions that are a mutually
4+recursive set of attributes used to create static asset directories
55with hashable file encodings. This file should be incorporated within
66your project's 'default.nix' file. (See ./example/default.nix , line 2)
77```
@@ -10,20 +10,20 @@ your project's 'default.nix' file. (See ./example/default.nix , line 2)
1010```
1111
1212The 'mkAssets' and 'mkAssetsWith' functions within 'assets.nix' only
13−need to be passed an encoding('zopfliEncodings', 'gzipEncodings',
13+need to be passed an encoding('zopfliEncodings', 'gzipEncodings',
1414'noEncodings') and a static directory of your choice as arguments.
1515
16−In this example, 'mkAssets' is used on a directory that has a .png
16+In this example, 'mkAssets' is used on a directory that has a .png
1717file inside of it. (See ./example/default.nix, line 11)
1818```
19−myAssets = assets.mkAssets ./static;
19+myAssets = assets.mkAssets ./static;
2020```
2121
22−Once you have successfully incorporated 'assets.nix' into your
23−project's nix file(s), use nix-build to generate a symlink of
24−your 'mkAssets' expression. The following command from
25−example/run-example (line 3) will generate an immutable symlink
26−that holds hashed static assets.
22+Once you have successfully incorporated 'assets.nix' into your
23+project's nix file(s), use nix-build to generate a symlink of
24+your 'mkAssets' expression. The following command from
25+example/run-example (line 3) will generate an immutable symlink
26+that holds hashed static assets.
2727```bash
2828nix-build -o static.assets -A myAssets
2929```
lib/cliapp/obelisk-cliapp.cabal+1
@@ -11,6 +11,7 @@ library
1111 , ansi-terminal
1212 , base
1313 , bytestring
14+ , containers
1415 , exceptions
1516 , io-streams
1617 , lens
lib/cliapp/src/Obelisk/CliApp.hs+14−7
@@ -33,17 +33,24 @@ module Obelisk.CliApp
3333 , Severity (..)
3434
3535 -- .Process
36− , ProcessFailure (..)
3736 , AsProcessFailure (..)
38− , readProcessAndLogStderr
39− , readProcessAndLogOutput
40− , readProcessJSONAndLogStderr
41− , readCreateProcessWithExitCode
37+ , ProcessFailure (..)
38+ , ProcessSpec (..)
39+ , callCommand
40+ , callProcess
4241 , callProcessAndLogOutput
4342 , createProcess_
44− , callProcess
45− , callCommand
43+ , overCreateProcess
44+ , proc
45+ , readCreateProcessWithExitCode
46+ , readProcessAndLogOutput
47+ , readProcessAndLogStderr
48+ , readProcessJSONAndLogStderr
4649 , reconstructCommand
50+ , setCwd
51+ , setDelegateCtlc
52+ , setEnvOverride
53+ , shell
4754 ) where
4855
4956import Control.Monad.Log (Severity (..))
lib/cliapp/src/Obelisk/CliApp/Demo.hs−1
@@ -8,7 +8,6 @@ import Control.Monad.Fail
88import Control.Monad.IO.Class
99import Data.Semigroup ((<>))
1010import qualified Data.Text as T
11−import System.Process (proc)
1211
1312import Control.Monad.Catch (MonadMask)
1413
lib/cliapp/src/Obelisk/CliApp/Process.hs+86−38
@@ -9,18 +9,26 @@
99-- | An extension of `System.Process` that integrates with logging (`Obelisk.CLI.Logging`)
1010-- and is thus spinner friendly.
1111module Obelisk.CliApp.Process
12− ( ProcessFailure (..)
13− , AsProcessFailure (..)
14− , readProcessAndLogStderr
15− , readProcessAndLogOutput
16− , readProcessJSONAndLogStderr
17− , readCreateProcessWithExitCode
12+ ( AsProcessFailure (..)
13+ , ProcessFailure (..)
14+ , ProcessFailure (..)
15+ , ProcessSpec (..)
16+ , callCommand
17+ , callProcess
1818 , callProcessAndLogOutput
1919 , createProcess
2020 , createProcess_
21− , callProcess
22− , callCommand
21+ , overCreateProcess
22+ , proc
23+ , readCreateProcessWithExitCode
24+ , readProcessAndLogOutput
25+ , readProcessAndLogStderr
26+ , readProcessJSONAndLogStderr
2327 , reconstructCommand
28+ , setCwd
29+ , setDelegateCtlc
30+ , setEnvOverride
31+ , shell
2432 ) where
2533
2634import Control.Monad ((<=<), join, void)
@@ -31,18 +39,20 @@ import Control.Lens (Prism', review)
3139import qualified Data.ByteString as BS
3240import qualified Data.ByteString.Char8 as BSC
3341import Data.Function (fix)
42+import Data.Map (Map)
43+import qualified Data.Map as Map
3444import Data.Monoid ((<>))
3545import Data.Text (Text)
3646import qualified Data.Text as T
3747import qualified Data.Text.Encoding as T
3848import Data.Text.Encoding.Error (lenientDecode)
49+import System.Environment (getEnvironment)
3950import System.Exit (ExitCode (..))
4051import System.IO (Handle)
4152import System.IO.Streams (InputStream, handleToInputStream)
4253import qualified System.IO.Streams as Streams
4354import System.IO.Streams.Concurrent (concurrentMerge)
44−import System.Process (CreateProcess, ProcessHandle, StdStream (CreatePipe), cmdspec, std_err, std_out,
45− waitForProcess)
55+import System.Process (CreateProcess, ProcessHandle, StdStream (CreatePipe), std_err, std_out, waitForProcess)
4656import qualified System.Process as Process
4757import qualified Data.Aeson as Aeson
4858
@@ -50,6 +60,30 @@ import Control.Monad.Log (Severity (..))
5060import Obelisk.CliApp.Logging (putLog, putLogRaw)
5161import Obelisk.CliApp.Types (CliLog, CliThrow)
5262
63+data ProcessSpec = ProcessSpec
64+ { _processSpec_createProcess :: !CreateProcess
65+ , _processSpec_overrideEnv :: !(Maybe (Map String String -> Map String String))
66+ }
67+
68+proc :: FilePath -> [String] -> ProcessSpec
69+proc cmd args = ProcessSpec (Process.proc cmd args) Nothing
70+
71+shell :: String -> ProcessSpec
72+shell cmd = ProcessSpec (Process.shell cmd) Nothing
73+
74+setEnvOverride :: (Map String String -> Map String String) -> ProcessSpec -> ProcessSpec
75+setEnvOverride f p = p { _processSpec_overrideEnv = Just f }
76+
77+overCreateProcess :: (CreateProcess -> CreateProcess) -> ProcessSpec -> ProcessSpec
78+overCreateProcess f (ProcessSpec p x) = ProcessSpec (f p) x
79+
80+setDelegateCtlc :: Bool -> ProcessSpec -> ProcessSpec
81+setDelegateCtlc b = overCreateProcess (\p -> p { Process.delegate_ctlc = b })
82+
83+setCwd :: Maybe FilePath -> ProcessSpec -> ProcessSpec
84+setCwd fp = overCreateProcess (\p -> p { Process.cwd = fp })
85+
86+
5387-- TODO put back in `Obelisk.CliApp.Process` and use prisms for extensible exceptions
5488data ProcessFailure = ProcessFailure Process.CmdSpec Int -- exit code
5589 deriving Show
@@ -64,15 +98,15 @@ instance AsProcessFailure ProcessFailure where
6498
6599readProcessAndLogStderr
66100 :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e, MonadFail m)
67− => Severity -> CreateProcess -> m Text
101+ => Severity -> ProcessSpec -> m Text
68102readProcessAndLogStderr sev process = do
69103 (out, _err) <- withProcess process $ \_out err -> do
70104 streamToLog =<< liftIO (streamHandle sev err)
71105 liftIO $ T.decodeUtf8With lenientDecode <$> BS.hGetContents out
72106
73107readProcessJSONAndLogStderr
74− :: (Aeson.FromJSON a, MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e)
75− => Severity -> CreateProcess -> m a
108+ :: (Aeson.FromJSON a, MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e, MonadFail m)
109+ => Severity -> ProcessSpec -> m a
76110readProcessJSONAndLogStderr sev process = do
77111 (out, _err) <- withProcess process $ \_out err -> do
78112 streamToLog =<< liftIO (streamHandle sev err)
@@ -81,13 +115,14 @@ readProcessJSONAndLogStderr sev process = do
81115 Right a -> pure a
82116 Left err -> do
83117 putLog Error $ "Could not decode process output as JSON: " <> T.pack err
84− throwError $ review asProcessFailure $ ProcessFailure (cmdspec process) 0
118+ throwError $ review asProcessFailure $ ProcessFailure (Process.cmdspec $ _processSpec_createProcess process) 0
85119
86120readCreateProcessWithExitCode
87121 :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e)
88− => CreateProcess -> m (ExitCode, String, String)
89−readCreateProcessWithExitCode process = do
90− putLog Debug $ "Creating process: " <> reconstructCommand (cmdspec process)
122+ => ProcessSpec -> m (ExitCode, String, String)
123+readCreateProcessWithExitCode procSpec = do
124+ process <- mkCreateProcess procSpec
125+ putLog Debug $ "Creating process: " <> reconstructProcSpec procSpec
91126 liftIO $ Process.readCreateProcessWithExitCode process ""
92127
93128-- | Like `System.Process.readProcess` but logs the combined output (stdout and stderr)
@@ -99,10 +134,10 @@ readCreateProcessWithExitCode process = do
99134-- `callProcessAndLogOutput (Debug, Debug)`.
100135readProcessAndLogOutput
101136 :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e, MonadFail m)
102− => (Severity, Severity) -> CreateProcess -> m Text
137+ => (Severity, Severity) -> ProcessSpec -> m Text
103138readProcessAndLogOutput (sev_out, sev_err) process = do
104− (_, Just out, Just err, p) <- createProcess $ process
105− { std_out = CreatePipe , std_err = CreatePipe }
139+ (_, Just out, Just err, p) <- createProcess $ overCreateProcess
140+ (\p -> p { std_out = CreatePipe , std_err = CreatePipe }) process
106141
107142 -- TODO interleave stdout and stderr in log correctly
108143 streamToLog =<< liftIO (streamHandle sev_err err)
@@ -111,9 +146,9 @@ readProcessAndLogOutput (sev_out, sev_err) process = do
111146
112147 liftIO (waitForProcess p) >>= \case
113148 ExitSuccess -> pure outText
114− ExitFailure code -> throwError $ review asProcessFailure $ ProcessFailure (cmdspec process) code
149+ ExitFailure code -> throwError $ review asProcessFailure $ ProcessFailure (Process.cmdspec $ _processSpec_createProcess process) code
115150
116−-- | Like `System.Process.callProcess` but logs the combined output (stdout and stderr)
151+-- | Like 'System.Process.callProcess' but logs the combined output (stdout and stderr)
117152-- with the corresponding severity.
118153--
119154-- Usually this function is called as `callProcessAndLogOutput (Debug, Error)`. However
@@ -121,9 +156,8 @@ readProcessAndLogOutput (sev_out, sev_err) process = do
121156-- which case it is advisable to call it with a non-Error severity for stderr, like
122157-- `callProcessAndLogOutput (Debug, Debug)`.
123158callProcessAndLogOutput
124−
125159 :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e, MonadFail m)
126− => (Severity, Severity) -> CreateProcess -> m ()
160+ => (Severity, Severity) -> ProcessSpec -> m ()
127161callProcessAndLogOutput (sev_out, sev_err) process =
128162 void $ withProcess process $ \out err -> do
129163 stream <- liftIO $ join $ combineStream
@@ -133,21 +167,31 @@ callProcessAndLogOutput (sev_out, sev_err) process =
133167 where
134168 combineStream s1 s2 = concurrentMerge [s1, s2]
135169
136−-- | Like `System.Process.createProcess` but also logs (debug) the process being run
170+-- | Like 'System.Process.createProcess' but also logs (debug) the process being run
137171createProcess
138172 :: (MonadIO m, CliLog m)
139− => CreateProcess -> m (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)
140−createProcess p = do
141− putLog Debug $ "Creating process: " <> reconstructCommand (cmdspec p)
173+ => ProcessSpec -> m (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)
174+createProcess procSpec = do
175+ p <- mkCreateProcess procSpec
176+ putLog Debug $ "Creating process: " <> reconstructProcSpec procSpec
142177 liftIO $ Process.createProcess p
143178
144179-- | Like `System.Process.createProcess_` but also logs (debug) the process being run
145180createProcess_
146181 :: (MonadIO m, CliLog m)
147− => String -> CreateProcess -> m (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)
148−createProcess_ name p = do
149− putLog Debug $ "Creating process " <> T.pack name <> ": " <> reconstructCommand (cmdspec p)
150− liftIO $ Process.createProcess p
182+ => String -> ProcessSpec -> m (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)
183+createProcess_ name procSpec = do
184+ p <- mkCreateProcess procSpec
185+ putLog Debug $ "Creating process " <> T.pack name <> ": " <> reconstructProcSpec procSpec
186+ liftIO $ Process.createProcess_ name p
187+
188+mkCreateProcess :: MonadIO m => ProcessSpec -> m Process.CreateProcess
189+mkCreateProcess (ProcessSpec p override') = do
190+ case override' of
191+ Nothing -> pure p
192+ Just override -> do
193+ procEnv <- Map.fromList <$> maybe (liftIO getEnvironment) pure (Process.env p)
194+ pure $ p { Process.env = Just $ Map.toAscList (override procEnv) }
151195
152196-- | Like `System.Process.callProcess` but also logs (debug) the process being run
153197callProcess
@@ -167,17 +211,17 @@ callCommand cmd = do
167211
168212withProcess
169213 :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e, MonadFail m)
170− => CreateProcess -> (Handle -> Handle -> m ()) -> m (Handle, Handle)
214+ => ProcessSpec -> (Handle -> Handle -> m ()) -> m (Handle, Handle)
171215withProcess process f = do -- TODO: Use bracket.
172216 -- FIXME: Using `withCreateProcess` here leads to something operating illegally on closed handles.
173− (_, Just out, Just err, p) <- createProcess $ process
174− { std_out = CreatePipe , std_err = CreatePipe }
217+ (_, Just out, Just err, p) <- createProcess $ overCreateProcess
218+ (\x -> x { std_out = CreatePipe , std_err = CreatePipe }) process
175219
176220 f out err -- Pass the handles to the passed function
177221
178222 liftIO (waitForProcess p) >>= \case
179223 ExitSuccess -> return (out, err)
180− ExitFailure code -> throwError $ review asProcessFailure $ ProcessFailure (cmdspec process) code
224+ ExitFailure code -> throwError $ review asProcessFailure $ ProcessFailure (Process.cmdspec $ _processSpec_createProcess process) code
181225
182226-- Create an input stream from the file handle, associating each item with the given severity.
183227streamHandle :: Severity -> Handle -> IO (InputStream (Severity, BSC.ByteString))
@@ -194,8 +238,12 @@ streamToLog stream = fix $ \loop -> do
194238
195239-- | Pretty print a 'CmdSpec'
196240reconstructCommand :: Process.CmdSpec -> Text
197−reconstructCommand (Process.ShellCommand str) = T.pack str
198−reconstructCommand (Process.RawCommand c as) = processToShellString c as
241+reconstructCommand p = case p of
242+ Process.ShellCommand str -> T.pack str
243+ Process.RawCommand c as -> processToShellString c as
199244 where
200245 processToShellString cmd args = T.unwords $ map quoteAndEscape (cmd : args)
201246 quoteAndEscape x = "'" <> T.replace "'" "'\''" (T.pack x) <> "'"
247+
248+reconstructProcSpec :: ProcessSpec -> Text
249+reconstructProcSpec = reconstructCommand . Process.cmdspec . _processSpec_createProcess
lib/command/obelisk-command.cabal+2−1
@@ -1,5 +1,5 @@
11name: obelisk-command
2−version: 0.1
2+version: 0.4
33cabal-version: >= 1.8
44build-type: Simple
55
@@ -46,6 +46,7 @@ library
4646 , transformers
4747 , unix
4848 , unordered-containers
49+ , which
4950 , yaml
5051 exposed-modules:
5152 Obelisk.App
lib/command/src/Obelisk/Command.hs+31−15
@@ -13,6 +13,7 @@ import qualified Data.Binary as Binary
1313import Data.Bool (bool)
1414import qualified Data.ByteString.Base16 as Base16
1515import qualified Data.ByteString.Lazy as LBS
16+import Data.Foldable (for_)
1617import Data.List
1718import Data.Maybe (catMaybes)
1819import qualified Data.Text as T
@@ -106,7 +107,7 @@ data ObInternal
106107inNixShell' :: MonadObelisk m => StaticPtr (ObeliskT IO ()) -> m ()
107108inNixShell' p = withProjectRoot "." $ \root -> do
108109 cmd <- liftIO $ unwords <$> mkCmd -- TODO: shell escape instead of unwords
109− projectShell root False "ghc" (Just cmd)
110+ projectShell root True "ghc" (Just cmd)
110111 where
111112 mkCmd = do
112113 argsCfg <- getArgsConfig
@@ -220,20 +221,35 @@ thunkDirectoryParser = fmap (dropTrailingPathSeparator . normalise) . strArgumen
220221 , help "Path to directory containing thunk data"
221222 ]
222223
224+thunkConfig :: Parser ThunkConfig
225+thunkConfig = ThunkConfig
226+ <$>
227+ ( flag' (Just True) (long "private" <> help "Mark thunks as pointing to a private repository")
228+ <|> flag' (Just False) (long "public" <> help "Mark thunks as pointing to a public repository")
229+ <|> pure Nothing
230+ )
231+
232+thunkUpdateConfig :: Parser ThunkUpdateConfig
233+thunkUpdateConfig = ThunkUpdateConfig
234+ <$> optional (strOption (long "branch" <> metavar "BRANCH" <> help "Use the given branch when looking for the latest revision"))
235+ <*> thunkConfig
236+
237+thunkPackConfig :: Parser ThunkPackConfig
238+thunkPackConfig = ThunkPackConfig
239+ <$> switch (long "force" <> short 'f' <> help "Force packing thunks even if there are branches not pushed upstream, uncommitted changes, stashes. This will cause changes that have not been pushed upstream to be lost; use with care.")
240+ <*> thunkConfig
241+
223242data ThunkCommand
224− = ThunkCommand_Update [FilePath] (Maybe String)
243+ = ThunkCommand_Update [FilePath] ThunkUpdateConfig
225244 | ThunkCommand_Unpack [FilePath]
226− | ThunkCommand_Pack [FilePath] Bool
245+ | ThunkCommand_Pack [FilePath] ThunkPackConfig
227246 deriving Show
228247
229−forceFlag :: Parser Bool
230−forceFlag = switch $ long "force" <> short 'f' <> help "Force packing thunks even if there are branches not pushed upstream, uncommitted changes, stashes. This will cause changes that have not been pushed upstream to be lost; use with care."
231−
232248thunkCommand :: Parser ThunkCommand
233249thunkCommand = hsubparser $ mconcat
234− [ command "update" $ info (ThunkCommand_Update <$> some thunkDirectoryParser <*> optional (strOption (long "branch" <> metavar "BRANCH"))) $ progDesc "Update thunk to latest revision available"
250+ [ command "update" $ info (ThunkCommand_Update <$> some thunkDirectoryParser <*> thunkUpdateConfig) $ progDesc "Update thunk to latest revision available"
235251 , command "unpack" $ info (ThunkCommand_Unpack <$> some thunkDirectoryParser) $ progDesc "Unpack thunk into git checkout of revision it points to"
236− , command "pack" $ info (ThunkCommand_Pack <$> some thunkDirectoryParser <*> forceFlag) $ progDesc "Pack git checkout into thunk that points at the current branch's upstream"
252+ , command "pack" $ info (ThunkCommand_Pack <$> some thunkDirectoryParser <*> thunkPackConfig) $ progDesc "Pack git checkout into thunk that points at the current branch's upstream"
237253 ]
238254
239255data ShellOpts
@@ -267,7 +283,8 @@ mkObeliskConfig :: IO Obelisk
267283mkObeliskConfig = do
268284 cliArgs <- getArgs
269285 -- This function should not use argument parser (full argument parsing happens post handoff)
270− let logLevel = toLogLevel $ "-v" `elem` cliArgs
286+ -- TODO: See if we can use the argument parser with a subset of the parsers to get logging level out.
287+ let logLevel = toLogLevel $ any (`elem` ["-v", "--verbose"]) cliArgs
271288 notInteractive <- not <$> isInteractiveTerm
272289 cliConf <- newCliConfig logLevel notInteractive notInteractive $ \case
273290 ObeliskError_ProcessError (ProcessFailure p code) ann ->
@@ -373,17 +390,16 @@ ob = \case
373390 DeployCommand_Update -> deployUpdate "."
374391 DeployCommand_Test (platform, extraArgs) -> deployMobile platform extraArgs
375392 ObCommand_Run -> inNixShell' $ static run
376− -- inNixShell ($(mkClosure 'ghcidAction) ())
377393 ObCommand_Thunk tc -> case tc of
378− ThunkCommand_Update thunks mBranch -> mapM_ ((flip updateThunkToLatest) mBranch) thunks
379− ThunkCommand_Unpack thunks -> mapM_ unpackThunk thunks
380− ThunkCommand_Pack thunks force -> forM_ thunks (packThunk force)
394+ ThunkCommand_Update thunks config -> for_ thunks (updateThunkToLatest config)
395+ ThunkCommand_Unpack thunks -> for_ thunks unpackThunk
396+ ThunkCommand_Pack thunks config -> for_ thunks (packThunk config)
381397 ObCommand_Repl -> runRepl
382398 ObCommand_Watch -> inNixShell' $ static runWatch
383399 ObCommand_Shell so -> withProjectRoot "." $ \root ->
384400 projectShell root False (_shellOpts_shell so) (_shellOpts_command so)
385− ObCommand_Doc shell pkgs -> withProjectRoot "." $ \root ->
386− projectShell root False shell (Just $ haddockCommand pkgs)
401+ ObCommand_Doc shell' pkgs -> withProjectRoot "." $ \root ->
402+ projectShell root False shell' (Just $ haddockCommand pkgs)
387403 ObCommand_Internal icmd -> case icmd of
388404 ObInternal_RunStaticIO k -> liftIO (unsafeLookupStaticPtr @(ObeliskT IO ()) k) >>= \case
389405 Nothing -> failWith $ "ObInternal_RunStaticIO: no such StaticKey: " <> T.pack (show k)
lib/command/src/Obelisk/Command/Deploy.hs+13−13
@@ -25,22 +25,23 @@ import Nix.Pretty
2525import Nix.String (principledMakeNixStringWithoutContext)
2626import Nix.Value
2727import System.Directory
28−import System.Environment (getEnvironment)
2928import System.FilePath
3029import System.IO
3130import System.Posix.Files
32−import System.Process (delegate_ctlc, env, proc, cwd)
3331import Text.URI (URI)
3432import qualified Text.URI as URI
3533import Text.URI.Lens
3634
3735import Obelisk.App (MonadObelisk)
38−import Obelisk.CliApp (Severity (..), callProcessAndLogOutput, failWith, putLog, withSpinner)
36+import Obelisk.CliApp (
37+ Severity (..), callProcessAndLogOutput, failWith, proc, putLog,
38+ setCwd, setDelegateCtlc, setEnvOverride, withSpinner)
3939import Obelisk.Command.Nix
4040import Obelisk.Command.Project
4141import Obelisk.Command.Thunk
4242import Obelisk.Command.Utils
4343
44+
4445deployInit
4546 :: MonadObelisk m
4647 => ThunkPtr
@@ -57,7 +58,7 @@ deployInit thunkPtr deployDir sshKeyPath hostnames route adminEmail enableHttps
5758 False -> failWith $ T.pack $ "ob deploy init: file does not exist: " <> sshKeyPath
5859 True -> pure $ deployDir </> "ssh_key"
5960 callProcessAndLogOutput (Notice, Error) $
60− proc "cp" [sshKeyPath, localKey]
61+ proc cp [sshKeyPath, localKey]
6162 liftIO $ setFileMode localKey $ ownerReadMode .|. ownerWriteMode
6263 return localKey
6364 withSpinner "Validating configuration" $ do
@@ -82,7 +83,7 @@ deployInit thunkPtr deployDir sshKeyPath hostnames route adminEmail enableHttps
8283 writeDeployConfig deployDir "backend_hosts" $ unlines hostnames
8384 writeDeployConfig deployDir "enable_https" $ show enableHttps
8485 writeDeployConfig deployDir "admin_email" adminEmail
85− writeDeployConfig deployDir ("config" </> "common" </> "route") $ route
86+ writeDeployConfig deployDir ("config" </> "common" </> "route") route
8687 withSpinner "Creating source thunk (./src)" $ liftIO $ do
8788 createThunk (deployDir </> "src") thunkPtr
8889 setupObeliskImpl deployDir
@@ -107,7 +108,7 @@ deployPush deployPath getNixBuilders = do
107108 Right (ThunkData_Packed ptr) -> return ptr
108109 Right (ThunkData_Checkout _) -> do
109110 checkGitCleanStatus srcPath True >>= \case
110− True -> packThunk False srcPath
111+ True -> packThunk (ThunkPackConfig False (ThunkConfig Nothing)) srcPath
111112 False -> failWith $ T.pack $ "ob deploy push: ensure " <> srcPath <> " has no pending changes and latest is pushed upstream."
112113 Left err -> failWith $ "ob deploy push: couldn't read src thunk: " <> T.pack (show err)
113114 let version = show . _thunkRev_commit $ _thunkPtr_rev thunkPtr
@@ -157,7 +158,7 @@ deployPush deployPath getNixBuilders = do
157158 ]
158159 isClean <- checkGitCleanStatus deployPath True
159160 when (not isClean) $ do
160− withSpinner "Commiting changes to Git" $ do
161+ withSpinner "Committing changes to Git" $ do
161162 callProcessAndLogOutput (Debug, Error) $ proc "git"
162163 ["-C", deployPath, "add", "."]
163164 callProcessAndLogOutput (Debug, Error) $ proc "git"
@@ -165,12 +166,11 @@ deployPush deployPath getNixBuilders = do
165166 putLog Notice $ "Deployed => " <> T.pack route
166167 where
167168 callProcess' envMap cmd args = do
168− processEnv <- Map.toList . (envMap <>) . Map.fromList <$> liftIO getEnvironment
169− let p = (proc cmd args) { delegate_ctlc = True, env = Just processEnv }
169+ let p = setEnvOverride (envMap <>) $ setDelegateCtlc True $ proc cmd args
170170 callProcessAndLogOutput (Notice, Notice) p
171171
172172deployUpdate :: MonadObelisk m => FilePath -> m ()
173−deployUpdate deployPath = updateThunkToLatest (deployPath </> "src") Nothing
173+deployUpdate deployPath = updateThunkToLatest (ThunkUpdateConfig Nothing (ThunkConfig Nothing)) (deployPath </> "src")
174174
175175keytoolToAndroidConfig :: KeytoolConfig -> HM.HashMap Text (NValue t Identity m)
176176keytoolToAndroidConfig conf =
@@ -219,7 +219,7 @@ deployMobile platform mobileArgs = withProjectRoot "." $ \root -> do
219219 , _keytoolConfig_storepass = keyStorePassword
220220 , _keytoolConfig_keypass = keyStorePassword
221221 }
222− createKeystore root $ keyToolConf
222+ createKeystore root keyToolConf
223223 liftIO $ BSL.writeFile keytoolConfPath $ encode keyToolConf
224224 checkKeytoolConfExist <- liftIO $ doesFileExist keytoolConfPath
225225 unless checkKeytoolConfExist $ failWith "Missing android KeytoolConfig"
@@ -275,13 +275,13 @@ instance ToJSON KeytoolConfig
275275createKeystore :: MonadObelisk m => FilePath -> KeytoolConfig -> m ()
276276createKeystore root config = do
277277 let expr = "with (import " <> toImplDir root <> ").reflex-platform.nixpkgs; pkgs.mkShell { buildInputs = [ pkgs.jdk ]; }"
278− callProcessAndLogOutput (Notice,Notice) $ (proc "nix-shell" ["default.nix", "-E" , expr, "--run" , keytoolCmd]) { cwd = Just root }
278+ callProcessAndLogOutput (Notice,Notice) $ setCwd (Just root) $ proc "nix-shell" ["default.nix", "-E" , expr, "--run" , keytoolCmd]
279279 where
280280 keytoolCmd = processToShellString "keytool"
281281 [ "-genkeypair", "-noprompt"
282282 , "-keystore", _keytoolConfig_keystore config
283283 , "-keyalg", "RSA", "-keysize", "2048"
284− , "-validity", "1000000000"
284+ , "-validity", "1000000"
285285 , "-storepass", _keytoolConfig_storepass config
286286 , "-alias", _keytoolConfig_alias config
287287 , "-keypass", _keytoolConfig_keypass config
lib/command/src/Obelisk/Command/Nix.hs+1−2
@@ -37,7 +37,6 @@ import Data.List (intercalate)
3737import Data.Maybe
3838import Data.Monoid ((<>))
3939import qualified Data.Text as T
40−import System.Process (proc)
4140
4241import Obelisk.App (MonadObelisk)
4342import Obelisk.CliApp
@@ -160,7 +159,7 @@ instance Default NixCmd where
160159
161160nixCmd :: MonadObelisk m => NixCmd -> m FilePath
162161nixCmd cmdCfg = withSpinner' ("Running " <> cmd <> desc) (Just $ const $ "Built " <> desc) $ do
163− output <- readProcessAndLogStderr Debug $ proc (T.unpack cmd) $ options
162+ output <- readProcessAndLogStderr Debug $ proc (T.unpack cmd) options
164163 -- Remove final newline that Nix appends
165164 Just (outPath, '\n') <- pure $ T.unsnoc output
166165 pure $ T.unpack outPath
lib/command/src/Obelisk/Command/Project.hs+77−33
@@ -1,6 +1,7 @@
11{-# LANGUAGE FlexibleContexts #-}
22{-# LANGUAGE LambdaCase #-}
33{-# LANGUAGE OverloadedStrings #-}
4+{-# LANGUAGE TemplateHaskell #-}
45module Obelisk.Command.Project
56 ( InitSource (..)
67 , initProject
@@ -15,6 +16,7 @@ module Obelisk.Command.Project
1516 , toImplDir
1617 ) where
1718
19+import Control.Concurrent.MVar (MVar, newMVar, withMVarMasked)
1820import Control.Monad
1921import Control.Monad.IO.Class (liftIO)
2022import Control.Monad.State
@@ -22,11 +24,14 @@ import Data.Bits
2224import Data.Function (on)
2325import qualified Data.Text as T
2426import System.Directory
27+import System.Environment (lookupEnv)
2528import System.FilePath
2629import System.IO.Temp
27−import System.Posix (FileStatus, UserID, deviceID, fileID, fileMode, fileOwner, getFileStatus, getRealUserID)
30+import System.IO.Unsafe (unsafePerformIO)
31+import System.Posix (FileStatus, FileMode, CMode (..), UserID, deviceID, fileID, fileMode, fileOwner, getFileStatus, getRealUserID)
2832import System.Posix.Files
29−import System.Process (CreateProcess, cwd, proc, waitForProcess, delegate_ctlc)
33+import System.Process (waitForProcess)
34+import System.Which (staticWhich)
3035
3136import GitHub.Data.GitData (Branch)
3237import GitHub.Data.Name (Name)
@@ -34,6 +39,7 @@ import GitHub.Data.Name (Name)
3439import Obelisk.App (MonadObelisk)
3540import Obelisk.CliApp
3641import Obelisk.Command.Thunk
42+
3743--TODO: Make this module resilient to random exceptions
3844
3945--TODO: Don't hardcode this
@@ -47,13 +53,14 @@ obeliskSourceWithBranch branch = ThunkSource_GitHub $ GitHubSource
4753 { _gitHubSource_owner = "obsidiansystems"
4854 , _gitHubSource_repo = "obelisk"
4955 , _gitHubSource_branch = Just branch
56+ , _gitHubSource_private = False
5057 }
5158
5259data InitSource
53− = InitSource_Default
54− | InitSource_Branch (Name Branch)
55− | InitSource_Symlink FilePath
56− deriving Show
60+ = InitSource_Default
61+ | InitSource_Branch (Name Branch)
62+ | InitSource_Symlink FilePath
63+ deriving Show
5764
5865-- | Path to obelisk directory in given path
5966toObeliskDir :: FilePath -> FilePath
@@ -114,15 +121,16 @@ initProject source force = withSystemTempDirectory "ob-init" $ \tmpDir -> do
114121findProjectObeliskCommand :: MonadObelisk m => FilePath -> m (Maybe FilePath)
115122findProjectObeliskCommand target = do
116123 myUid <- liftIO getRealUserID
124+ processUmask <- liftIO getUmask
117125 targetStat <- liftIO $ getFileStatus target
118− (result, insecurePaths) <- flip runStateT [] $ walkToProjectRoot target targetStat myUid >>= \case
126+ (result, insecurePaths) <- flip runStateT [] $ walkToProjectRoot target targetStat processUmask myUid >>= \case
119127 Nothing -> pure Nothing
120128 Just projectRoot -> liftIO (doesDirectoryExist $ toImplDir projectRoot) >>= \case
121129 False -> do
122130 putLog Warning $ "Found obelisk directory in " <> T.pack projectRoot <> " but the implementation (impl) file is missing"
123131 pure Nothing
124132 True -> do
125− walkToImplDir projectRoot myUid -- For security check
133+ walkToImplDir projectRoot myUid processUmask -- For security check
126134 return $ Just projectRoot
127135 case (result, insecurePaths) of
128136 (Just projDir, []) -> do
@@ -133,16 +141,36 @@ findProjectObeliskCommand target = do
133141 putLog Error $ T.unlines
134142 [ "Error: Found a project at " <> T.pack (normalise projDir) <> ", but had to traverse one or more insecure directories to get there:"
135143 , T.unlines $ fmap (T.pack . normalise) insecurePaths
136− , "Please ensure that all of these directories are owned by you and are not writable by anyone else."
144+ , "Please ensure that all of these directories are owned by you, not world-writable, and no more group-writable than permitted by your umask."
137145 ]
138146 return Nothing
139147
148+-- | Get the umask for the Obelisk process.
149+--
150+-- Because of
151+-- http://man7.org/linux/man-pages/man2/umask.2.html#NOTES we have to set the
152+-- umask to read it. We are using 'withMVarMasked' to guarantee that setting and
153+-- reading isn't interrupted by any exception or interleaved with another thread.
154+getUmask :: IO FileMode
155+getUmask = withMVarMasked globalUmaskLock $ \() -> do
156+ initialMask <- setFileCreationMask safeUmask
157+ void (setFileCreationMask initialMask)
158+ pure initialMask
159+ where
160+ safeUmask :: FileMode
161+ safeUmask = CMode 0o22
162+
163+{-# NOINLINE globalUmaskLock #-}
164+globalUmaskLock :: MVar ()
165+globalUmaskLock = unsafePerformIO (newMVar ())
166+
140167-- | Get the FilePath to the containing project directory, if there is one
141168findProjectRoot :: MonadObelisk m => FilePath -> m (Maybe FilePath)
142169findProjectRoot target = do
143170 myUid <- liftIO getRealUserID
144171 targetStat <- liftIO $ getFileStatus target
145− (result, _) <- liftIO $ runStateT (walkToProjectRoot target targetStat myUid) []
172+ umask <- liftIO getUmask
173+ (result, _) <- liftIO $ runStateT (walkToProjectRoot target targetStat umask myUid) []
146174 return result
147175
148176withProjectRoot :: MonadObelisk m => FilePath -> (FilePath -> m a) -> m a
@@ -155,15 +183,15 @@ withProjectRoot target f = findProjectRoot target >>= \case
155183-- traversed in the process. Return the project root directory, if found.
156184walkToProjectRoot
157185 :: (MonadState [FilePath] m, MonadIO m)
158− => FilePath -> FileStatus -> UserID -> m (Maybe FilePath)
159−walkToProjectRoot this thisStat myUid = liftIO (doesDirectoryExist this) >>= \case
186+ => FilePath -> FileStatus -> FileMode -> UserID -> m (Maybe FilePath)
187+walkToProjectRoot this thisStat desiredUmask myUid = liftIO (doesDirectoryExist this) >>= \case
160188 -- It's not a directory, so it can't be a project
161189 False -> do
162190 let dir = takeDirectory this
163191 dirStat <- liftIO $ getFileStatus dir
164− walkToProjectRoot dir dirStat myUid
192+ walkToProjectRoot dir dirStat desiredUmask myUid
165193 True -> do
166− when (not $ isWritableOnlyBy thisStat myUid) $ modify (this:)
194+ when (not $ isWellOwnedAndWellPermissioned thisStat myUid desiredUmask) $ modify (this:)
167195 liftIO (doesDirectoryExist $ toObeliskDir this) >>= \case
168196 True -> return $ Just this
169197 False -> do
@@ -173,25 +201,38 @@ walkToProjectRoot this thisStat myUid = liftIO (doesDirectoryExist this) >>= \ca
173201 isSameFileAs = (==) `on` fileIdentity
174202 if thisStat `isSameFileAs` nextStat
175203 then return Nothing -- Found a cycle; probably hit root directory
176− else walkToProjectRoot next nextStat myUid
204+ else walkToProjectRoot next nextStat desiredUmask myUid
177205
178206-- | Walk from the given project root directory to its Obelisk implementation
179207-- directory, accumulating potentially insecure directories that were traversed
180208-- in the process.
181−walkToImplDir :: (MonadState [FilePath] m, MonadIO m) => FilePath -> UserID -> m ()
182−walkToImplDir projectRoot myUid = do
209+walkToImplDir :: (MonadState [FilePath] m, MonadIO m) => FilePath -> UserID -> FileMode -> m ()
210+walkToImplDir projectRoot myUid umask = do
183211 let obDir = toObeliskDir projectRoot
184212 obDirStat <- liftIO $ getFileStatus obDir
185− when (not $ isWritableOnlyBy obDirStat myUid) $ modify (obDir:)
213+ when (not $ isWellOwnedAndWellPermissioned obDirStat myUid umask) $ modify (obDir:)
186214 let implThunk = obDir </> "impl"
187215 implThunkStat <- liftIO $ getFileStatus implThunk
188− when (not $ isWritableOnlyBy implThunkStat myUid) $ modify (implThunk:)
216+ when (not $ isWellOwnedAndWellPermissioned implThunkStat myUid umask) $ modify (implThunk:)
217+
218+-- | Check to see if directory is writable by a user whose User ID matches the
219+-- second argument provided, and if the fact that other people can write to that
220+-- directory is in accordance with the umask of the system, passed as the third
221+-- argument.
222+isWellOwnedAndWellPermissioned :: FileStatus -> UserID -> FileMode -> Bool
223+isWellOwnedAndWellPermissioned s uid umask = isOwnedBy s uid && filePermissionIsSafe s umask
189224
190−--TODO: Is there a better way to ask if anyone else can write things?
191−--E.g. what about ACLs?
192−-- | Check to see if directory is only writable by a user whose User ID matches the second argument provided
193−isWritableOnlyBy :: FileStatus -> UserID -> Bool
194−isWritableOnlyBy s uid = fileOwner s == uid && fileMode s .&. 0o22 == 0
225+isOwnedBy :: FileStatus -> UserID -> Bool
226+isOwnedBy s uid = fileOwner s == uid
227+
228+-- | Check to see if a directory respect the umask, but check explicitly that
229+-- it's not world writable in any case.
230+filePermissionIsSafe :: FileStatus -> FileMode -> Bool
231+filePermissionIsSafe s umask = not fileWorldWritable && fileGroupWritable <= umaskGroupWritable
232+ where
233+ fileWorldWritable = fileMode s .&. 0o002 == 0o002
234+ fileGroupWritable = fileMode s .&. 0o020 == 0o020
235+ umaskGroupWritable = umask .&. 0o020 == 0
195236
196237-- | Run a command in the given shell for the current project
197238inProjectShell :: MonadObelisk m => String -> String -> m ()
@@ -204,18 +245,21 @@ inImpureProjectShell shellName command = withProjectRoot "." $ \root ->
204245
205246projectShell :: MonadObelisk m => FilePath -> Bool -> String -> Maybe String -> m ()
206247projectShell root isPure shellName command = do
207− (_, _, _, ph) <- createProcess_ "runNixShellAttr" $ setCtlc $ setCwd (Just root) $ proc "nix-shell" $
248+ let nixPath = $(staticWhich "nix")
249+ nixpkgsPath <- fmap T.strip $ readProcessAndLogStderr Debug $ setCwd (Just root) $ proc nixPath ["eval", "(import .obelisk/impl {}).nixpkgs.path"]
250+ nixRemote <- liftIO $ lookupEnv "NIX_REMOTE"
251+ (_, _, _, ph) <- createProcess_ "runNixShellAttr" $ setDelegateCtlc True $ setCwd (Just root) $ proc "nix-shell" $
208252 [ "default.nix"] <>
209− [ "--pure" | isPure ] <>
253+ ["--pure" | isPure] <>
210254 [ "-A"
211255 , "shells." <> shellName
212256 ] <> case command of
213257 Nothing -> []
214− Just c -> ["--run", c]
258+ Just c ->
259+ -- TODO: Escape nixpkgsPath and nixRemote
260+ [ "--run"
261+ , "export NIX_PATH=nixpkgs=" <> T.unpack nixpkgsPath <> "; " <>
262+ maybe "" (\v -> "export NIX_REMOTE=" <> v <> "; ") nixRemote <>
263+ c
264+ ]
215265 void $ liftIO $ waitForProcess ph
216−
217−setCtlc :: CreateProcess -> CreateProcess
218−setCtlc cfg = cfg { delegate_ctlc = True }
219−
220−setCwd :: Maybe FilePath -> CreateProcess -> CreateProcess
221−setCwd fp cfg = cfg { cwd = fp }
lib/command/src/Obelisk/Command/Run.hs+90−71
@@ -3,13 +3,18 @@
33{-# LANGUAGE MultiParamTypeClasses #-}
44{-# LANGUAGE OverloadedStrings #-}
55{-# LANGUAGE ScopedTypeVariables #-}
6+{-# LANGUAGE TemplateHaskell #-}
7+{-# LANGUAGE TupleSections #-}
68module Obelisk.Command.Run where
79
8−import Control.Exception (bracket)
10+import Control.Applicative (liftA2)
11+import Control.Exception (Exception, bracket)
912import Control.Monad
13+import Control.Monad.Except (runExceptT, throwError)
1014import Control.Monad.IO.Class (liftIO)
1115import Control.Monad.Reader (MonadIO)
1216import Data.Either
17+import Data.Foldable (for_)
1318import qualified Data.List as L
1419import qualified Data.List.NonEmpty as NE
1520import Data.Maybe
@@ -23,22 +28,18 @@ import Distribution.Types.CondTree
2328import Distribution.Types.GenericPackageDescription
2429import Distribution.Types.Library
2530import Distribution.Utils.Generic
26−import Hpack.Config
27−import Hpack.Render
28−import Hpack.Yaml
31+import qualified Hpack.Config as Hpack
32+import qualified Hpack.Render as Hpack
33+import qualified Hpack.Yaml as Hpack
2934import Language.Haskell.Extension
3035import Network.Socket hiding (Debug)
3136import System.Directory
3237import System.FilePath
33−import System.Process (proc)
3438import System.IO.Temp (withSystemTempDirectory)
35−import Data.ByteString (ByteString)
39+import System.Which (staticWhich)
3640
3741import Obelisk.App (MonadObelisk, ObeliskT)
38−import Obelisk.CliApp
39− ( CliT (..), HasCliConfig, Severity (..)
40− , callCommand, failWith, getCliConfig, putLog
41− , readProcessAndLogStderr, readProcessJSONAndLogStderr, runCli)
42+import Obelisk.CliApp (Severity (..) , callCommand, failWith, putLog, proc, readProcessAndLogStderr, readProcessJSONAndLogStderr)
4243import Obelisk.Command.Project (inProjectShell, withProjectRoot)
4344
4445data CabalPackageInfo = CabalPackageInfo
@@ -56,13 +57,15 @@ data CabalPackageInfo = CabalPackageInfo
5657-- NOTE: `run` is not polymorphic like the rest because we use StaticPtr to invoke it.
5758run :: ObeliskT IO ()
5859run = withProjectRoot "." $ \root -> do
60+ let nixPath = $(staticWhich "nix")
61+ nixBuildPath = $(staticWhich "nix-build")
5962 pkgs <- getLocalPkgs root
6063 withGhciScript pkgs $ \dotGhciPath -> do
6164 freePort <- getFreePort
6265 assets <- do
6366 let importableRoot = toNixPath root
6467 isDerivation <- readProcessAndLogStderr Debug $
65− proc "nix"
68+ proc nixPath
6669 [ "eval"
6770 , "-f"
6871 , root
@@ -74,12 +77,12 @@ run = withProjectRoot "." $ \root -> do
7477 -- Check whether the impure static files are a derivation (and so must be built)
7578 if isDerivation == "1"
7679 then fmap T.strip $ readProcessAndLogStderr Debug $ -- Strip whitespace here because nix-build has no --raw option
77− proc "nix-build"
80+ proc nixBuildPath
7881 [ "--no-out-link"
7982 , "-E", "(import " <> importableRoot <> "{}).passthru.staticFilesImpure"
8083 ]
8184 else readProcessAndLogStderr Debug $
82− proc "nix" ["eval", "-f", root, "passthru.staticFilesImpure", "--raw"]
85+ proc nixPath ["eval", "-f", root, "passthru.staticFilesImpure", "--raw"]
8386 putLog Debug $ "Assets impurely loaded from: " <> assets
8487 runGhcid dotGhciPath $ Just $ unwords
8588 [ "Obelisk.Run.run"
@@ -129,71 +132,79 @@ getLocalPkgs root = do
129132predefinedLocalPkgs :: [FilePath]
130133predefinedLocalPkgs = ["backend", "common", "frontend"]
131134
135+
136+data GuessPackageFileError = GuessPackageFileError_Ambiguous [FilePath] | GuessPackageFileError_NotFound
137+ deriving (Eq, Ord, Show)
138+instance Exception GuessPackageFileError
139+
140+-- | Given a directory, try to guess what the appropriate @.cabal@ or @package.yaml@ file is for the package.
141+guessCabalPackageFile
142+ :: MonadIO m
143+ => FilePath -- ^ Directory to search for cabal package
144+ -> m (Either GuessPackageFileError (Either FilePath FilePath))
145+ -- ^ 'Right' 'Left' for hpack package, 'Right' 'Right' for cabal package; 'FilePath' is relative to given directory.
146+guessCabalPackageFile dir = do
147+ candidates <- liftIO $
148+ filterM (doesFileExist . (dir </>))
149+ =<< filter (liftA2 (||) (== Hpack.packageConfig) (".cabal" `L.isSuffixOf`))
150+ <$> listDirectory dir
151+ pure $ case L.partition (== Hpack.packageConfig) candidates of
152+ ([hpack], _) -> Right $ Left hpack
153+ ([], [cabal]) -> Right $ Right cabal
154+ ([], []) -> Left GuessPackageFileError_NotFound
155+ (hpacks, cabals) -> Left $ GuessPackageFileError_Ambiguous $ hpacks <> cabals
156+
132157parseCabalPackage
133158 :: (MonadObelisk m)
134159 => FilePath -- ^ package directory
135160 -> m (Maybe CabalPackageInfo)
136−parseCabalPackage dir = do
137− let cabalFp = dir </> (takeBaseName dir <> ".cabal")
138− hpackFp = dir </> "package.yaml"
139− hasCabal <- liftIO $ doesFileExist cabalFp
140− hasHpack <- liftIO $ doesFileExist hpackFp
161+parseCabalPackage dir = either ((Nothing <$) . putLog Error) (pure . Just) <=< runExceptT $ do
162+ (cabalContents, packageFile) <- guessCabalPackageFile dir >>= \case
163+ Left GuessPackageFileError_NotFound -> throwError $ "No .cabal or package.yaml file found in " <> T.pack dir
164+ Left (GuessPackageFileError_Ambiguous _) -> throwError $ "Unable to determine which .cabal file to use in " <> T.pack dir
165+ Right (Right cabalFileName) -> let file = dir </> cabalFileName in (, file) <$> liftIO (readUTF8File file)
166+ Right (Left hpackFileName) -> do
167+ let
168+ file = dir </> hpackFileName
169+ decodeOptions = Hpack.DecodeOptions (Hpack.ProgramName "ob") file Nothing Hpack.decodeYaml
170+ liftIO (Hpack.readPackageConfig decodeOptions) >>= \case
171+ Left err -> throwError $ T.pack $ "Failed to parse " <> file <> ": " <> err
172+ Right (Hpack.DecodeResult hpackPackage _ _ _) -> pure (Hpack.renderPackage [] hpackPackage, file)
141173
142− mCabalContents <- if hasCabal
143− then Just <$> liftIO (readUTF8File cabalFp)
144− else if hasHpack
145− then do
146− let decodeOptions = DecodeOptions (ProgramName "ob") hpackFp Nothing decodeYaml
147− liftIO (readPackageConfig decodeOptions) >>= \case
148− Left err -> do
149− putLog Error $ T.pack $ "Failed to parse " <> hpackFp <> ": " <> err
150− return Nothing
151− Right (DecodeResult hpackPackage _ _ _) -> do
152− return $ Just $ renderPackage [] hpackPackage
153− else return Nothing
154−
155− fmap join $ forM mCabalContents $ \cabalContents -> do
156− let (warnings, result) = runParseResult $ parseGenericPackageDescription $
157− toUTF8BS $ cabalContents
158− mapM_ (putLog Warning) $ fmap (T.pack . show) warnings
159− case result of
160− Right gpkg -> do
161− return $ do
162− (_, lib) <- simplifyCondTree (const $ pure True) <$> condLibrary gpkg
163− pure $ CabalPackageInfo
164− { _cabalPackageInfo_packageRoot = takeDirectory cabalFp
165− , _cabalPackageInfo_sourceDirs =
166− fromMaybe (pure ".") $ NE.nonEmpty $ hsSourceDirs $ libBuildInfo lib
167− , _cabalPackageInfo_defaultExtensions =
168− defaultExtensions $ libBuildInfo lib
169− , _cabalPackageInfo_defaultLanguage =
170− defaultLanguage $ libBuildInfo lib
171− , _cabalPackageInfo_compilerOptions = options $ libBuildInfo lib
172− }
173− Left (_, errors) -> do
174− putLog Error $ T.pack $ "Failed to parse " <> cabalFp <> ":"
175− mapM_ (putLog Error) $ fmap (T.pack . show) errors
176− return Nothing
177−
178−withUTF8FileContentsM :: (MonadIO m, HasCliConfig e m) => FilePath -> (ByteString -> CliT e IO a) -> m a
179−withUTF8FileContentsM fp f = do
180− c <- getCliConfig
181− liftIO $ withUTF8FileContents fp $ runCli c . f . toUTF8BS
174+ let (warnings, result) = runParseResult $ parseGenericPackageDescription $
175+ toUTF8BS cabalContents
176+ for_ warnings $ putLog Warning . T.pack . show
177+ case condLibrary <$> result of
178+ Right (Just condLib) -> do
179+ let (_, lib) = simplifyCondTree (const $ pure True) condLib
180+ pure $ CabalPackageInfo
181+ { _cabalPackageInfo_packageRoot = dir
182+ , _cabalPackageInfo_sourceDirs =
183+ fromMaybe (pure ".") $ NE.nonEmpty $ hsSourceDirs $ libBuildInfo lib
184+ , _cabalPackageInfo_defaultExtensions =
185+ defaultExtensions $ libBuildInfo lib
186+ , _cabalPackageInfo_defaultLanguage =
187+ defaultLanguage $ libBuildInfo lib
188+ , _cabalPackageInfo_compilerOptions = options $ libBuildInfo lib
189+ }
190+ Right Nothing -> throwError "Haskell package has no library component"
191+ Left (_, errors) ->
192+ throwError $ T.pack $ "Failed to parse " <> packageFile <> ":\n" <> unlines (map show errors)
182193
183194-- | Create ghci configuration to load the given packages
184195withGhciScript
185196 :: MonadObelisk m
186197 => [FilePath] -- ^ List of packages to load into ghci
187− -> (FilePath -> m ()) -- ^ Action to run with the path to generated temporory .ghci
198+ -> (FilePath -> m ()) -- ^ Action to run with the path to generated temporary .ghci
188199 -> m ()
189−withGhciScript pkgs f = do
190− (pkgDirErrs, packageInfos) <- fmap partitionEithers $ forM pkgs $ \pkg -> do
191− flip fmap (parseCabalPackage pkg) $ \case
192− Nothing -> Left pkg
200+withGhciScript dirs f = do
201+ (pkgDirErrs, packageInfos) <- fmap partitionEithers $ forM dirs $ \dir -> do
202+ flip fmap (parseCabalPackage dir) $ \case
203+ Nothing -> Left dir
193204 Just packageInfo -> Right packageInfo
194205
195206 when (null packageInfos) $
196− failWith $ T.pack $ "No valid pkgs found in " <> intercalate ", " pkgs
207+ failWith $ T.pack $ "No valid pkgs found in " <> intercalate ", " dirs
197208 unless (null pkgDirErrs) $
198209 putLog Warning $ T.pack $ "Failed to find pkgs in " <> intercalate ", " pkgDirErrs
199210
@@ -203,16 +214,16 @@ withGhciScript pkgs f = do
203214 language = NE.toList $ fromMaybe (Haskell98 NE.:| []) $ NE.nonEmpty languageFromPkgs
204215 extensionsLine = if extensions == mempty
205216 then ""
206− else ":set " <> intercalate " " ((("-X" <>) . prettyShow) <$> extensions)
217+ else ":set " <> unwords (("-X" <>) . prettyShow <$> extensions)
207218 ghcOptions = concat $ mapMaybe (\case (GHC, xs) -> Just xs; _ -> Nothing) $
208219 packageInfos >>= _cabalPackageInfo_compilerOptions
209− dotGhci = unlines $
220+ dotGhci = unlines
210221 [ ":set -i" <> intercalate ":" (packageInfos >>= rootedSourceDirs)
211222 , case ghcOptions of
212223 [] -> ""
213− xs -> ":set " <> intercalate " " xs
224+ xs -> ":set " <> unwords xs
214225 , extensionsLine
215− , ":set " <> intercalate " " (("-X" <>) . prettyShow <$> language)
226+ , ":set " <> unwords (("-X" <>) . prettyShow <$> language)
216227 , ":load Backend Frontend"
217228 , "import Obelisk.Run"
218229 , "import qualified Frontend"
@@ -237,7 +248,7 @@ runGhciRepl
237248 :: MonadObelisk m
238249 => FilePath -- ^ Path to .ghci
239250 -> m ()
240−runGhciRepl dotGhci = inProjectShell "ghc" $ unwords $ "ghci" : ["-no-user-package-db", "-ghci-script", dotGhci]
251+runGhciRepl dotGhci = inProjectShell "ghc" $ "ghci " <> makeBaseGhciOptions dotGhci
241252
242253-- | Run ghcid
243254runGhcid
@@ -245,17 +256,25 @@ runGhcid
245256 => FilePath -- ^ Path to .ghci
246257 -> Maybe String -- ^ Optional command to run at every reload
247258 -> m ()
248−runGhcid dotGhci mcmd = callCommand $ unwords $ "ghcid" : opts
259+runGhcid dotGhci mcmd = callCommand $ unwords $ $(staticWhich "ghcid") : opts
249260 where
250261 opts =
251262 [ "-W"
252263 --TODO: The decision of whether to use -fwarn-redundant-constraints should probably be made by the user
253− , "--command='ghci -Wall -ignore-dot-ghci -fwarn-redundant-constraints -no-user-package-db -ghci-script " <> dotGhci <> "' "
264+ , "--command='ghci -Wall -ignore-dot-ghci -fwarn-redundant-constraints " <> makeBaseGhciOptions dotGhci <> "' "
254265 , "--reload=config"
255266 , "--outputfile=ghcid-output.txt"
256267 ] <> testCmd
257268 testCmd = maybeToList (flip fmap mcmd $ \cmd -> "--test='" <> cmd <> "'")
258269
270+makeBaseGhciOptions :: FilePath -> String
271+makeBaseGhciOptions dotGhci =
272+ unwords
273+ [ "-no-user-package-db"
274+ , "-package-env -"
275+ , "-ghci-script " <> dotGhci
276+ ]
277+
259278getFreePort :: MonadIO m => m PortNumber
260279getFreePort = liftIO $ withSocketsDo $ do
261280 addr:_ <- getAddrInfo (Just defaultHints) (Just "127.0.0.1") (Just "0")
lib/command/src/Obelisk/Command/Thunk.hs+180−102
@@ -8,25 +8,30 @@
88{-# LANGUAGE ScopedTypeVariables #-}
99{-# LANGUAGE TypeApplications #-}
1010module Obelisk.Command.Thunk
11− ( ThunkPtr (..)
11+ ( GitHubSource (..)
12+ , GitUri (..)
13+ , ReadThunkError (..)
14+ , ThunkConfig (..)
15+ , ThunkData (..)
16+ , ThunkPackConfig (..)
17+ , ThunkPtr (..)
1218 , ThunkRev (..)
1319 , ThunkSource (..)
14− , ThunkData (..)
15− , ReadThunkError (..)
16− , GitHubSource (..)
17− , getThunkGitBranch
18− , getLatestRev
19− , updateThunkToLatest
20+ , ThunkUpdateConfig (..)
2021 , createThunk
2122 , createThunkWithLatest
23+ , getLatestRev
24+ , getThunkGitBranch
25+ , getThunkPtr
26+ , gitUriToText
2227 , nixBuildAttrWithCache
2328 , nixBuildThunkAttrWithCache
24− , unpackThunk
2529 , packThunk
30+ , parseGitUri
2631 , readThunk
32+ , unpackThunk
2733 , updateThunk
28− , getThunkPtr
29− , parseGitUri
34+ , updateThunkToLatest
3035 , uriThunkPtr
3136 ) where
3237
@@ -68,8 +73,7 @@ import System.FilePath
6873import System.IO.Error
6974import System.IO.Temp
7075import System.Posix (getSymbolicLinkStatus, modificationTime)
71−import System.Process (proc)
72−import Text.URI
76+import qualified Text.URI as URI
7377
7478import Obelisk.App (MonadObelisk)
7579import Obelisk.CliApp
@@ -111,37 +115,63 @@ data GitHubSource = GitHubSource
111115 { _gitHubSource_owner :: Name Owner
112116 , _gitHubSource_repo :: Name Repo
113117 , _gitHubSource_branch :: Maybe (Name Branch)
118+ , _gitHubSource_private :: Bool
114119 }
115120 deriving (Show, Eq, Ord)
116121
122+newtype GitUri = GitUri URI.URI deriving (Eq, Ord, Show)
123+
124+gitUriToText :: GitUri -> Text
125+gitUriToText (GitUri uri)
126+ | (T.toLower . URI.unRText <$> URI.uriScheme uri) == Just "file"
127+ , Just (_, path) <- URI.uriPath uri
128+ = "/" <> T.intercalate "/" (map URI.unRText $ NonEmpty.toList path)
129+ | otherwise = URI.render uri
130+
117131data GitSource = GitSource
118− { _gitSource_url :: URI
132+ { _gitSource_url :: GitUri
119133 , _gitSource_branch :: Maybe (Name Branch)
120134 , _gitSource_fetchSubmodules :: Bool
135+ , _gitSource_private :: Bool
121136 }
122137 deriving (Show, Eq, Ord)
123138
139+newtype ThunkConfig = ThunkConfig
140+ { _thunkConfig_private :: Maybe Bool
141+ } deriving Show
142+
143+data ThunkUpdateConfig = ThunkUpdateConfig
144+ { _thunkUpdateConfig_branch :: Maybe String
145+ , _thunkUpdateConfig_config :: ThunkConfig
146+ } deriving Show
147+
148+data ThunkPackConfig = ThunkPackConfig
149+ { _thunkPackConfig_force :: Bool
150+ , _thunkPackConfig_config :: ThunkConfig
151+ } deriving Show
152+
124153-- | Convert a GitHub source to a regular Git source. Assumes no submodules.
125154forgetGithub :: Bool -> GitHubSource -> GitSource
126155forgetGithub useSsh s = GitSource
127− { _gitSource_url = URI
128− { uriScheme = Just $ fromRight' $ mkScheme $ if useSsh then "ssh" else "https"
129− , uriAuthority = Right $ Authority
130− { authUserInfo = UserInfo (fromRight' $ mkUsername "git") Nothing
156+ { _gitSource_url = GitUri $ URI.URI
157+ { URI.uriScheme = Just $ fromRight' $ URI.mkScheme $ if useSsh then "ssh" else "https"
158+ , URI.uriAuthority = Right $ URI.Authority
159+ { URI.authUserInfo = URI.UserInfo (fromRight' $ URI.mkUsername "git") Nothing
131160 <$ guard useSsh
132− , authHost = fromRight' $ mkHost "github.com"
133− , authPort = Nothing
161+ , URI.authHost = fromRight' $ URI.mkHost "github.com"
162+ , URI.authPort = Nothing
134163 }
135− , uriPath = Just ( False
136− , fromRight' . mkPathPiece <$>
164+ , URI.uriPath = Just ( False
165+ , fromRight' . URI.mkPathPiece <$>
137166 untagName (_gitHubSource_owner s)
138167 :| [ untagName (_gitHubSource_repo s) <> ".git" ]
139168 )
140− , uriQuery = []
141− , uriFragment = Nothing
169+ , URI.uriQuery = []
170+ , URI.uriFragment = Nothing
142171 }
143172 , _gitSource_branch = _gitHubSource_branch s
144173 , _gitSource_fetchSubmodules = False
174+ , _gitSource_private = _gitHubSource_private s
145175 }
146176
147177getThunkGitBranch :: ThunkPtr -> Maybe Text
@@ -155,20 +185,20 @@ commitNameToRef (N c) = Ref.fromHex $ encodeUtf8 c
155185-- TODO: Use spinner here.
156186getNixSha256ForUriUnpacked
157187 :: MonadObelisk m
158− => URI
188+ => GitUri
159189 -> m NixSha256
160190getNixSha256ForUriUnpacked uri =
161− withExitFailMessage ("nix-prefetch-url: Failed to determine sha256 hash of URL " <> render uri) $ do
191+ withExitFailMessage ("nix-prefetch-url: Failed to determine sha256 hash of URL " <> gitUriToText uri) $ do
162192 [hash] <- fmap T.lines $ readProcessAndLogOutput (Debug, Debug) $
163− proc "nix-prefetch-url" ["--unpack" , "--type" , "sha256" , T.unpack $ render uri]
193+ proc "nix-prefetch-url" ["--unpack", "--type", "sha256", T.unpack $ gitUriToText uri]
164194 pure hash
165195
166−nixPrefetchGit :: MonadObelisk m => URI -> Text -> Bool -> m NixSha256
196+nixPrefetchGit :: MonadObelisk m => GitUri -> Text -> Bool -> m NixSha256
167197nixPrefetchGit uri rev fetchSubmodules =
168− withExitFailMessage ("nix-prefetch-git: Failed to determine sha256 hash of Git repo " <> render uri <> " at " <> rev) $ do
198+ withExitFailMessage ("nix-prefetch-git: Failed to determine sha256 hash of Git repo " <> gitUriToText uri <> " at " <> rev) $ do
169199 out <- readProcessAndLogStderr Debug $
170200 proc "nix-prefetch-git" $ filter (/="")
171− [ "--url", T.unpack $ render uri
201+ [ "--url", T.unpack $ gitUriToText uri
172202 , "--rev", T.unpack rev
173203 , if fetchSubmodules then "--fetch-submodules" else ""
174204 , "--quiet"
@@ -286,10 +316,12 @@ parseGitHubSource v = do
286316 owner <- v Aeson..: "owner"
287317 repo <- v Aeson..: "repo"
288318 branch <- v Aeson..:! "branch"
319+ private <- v Aeson..:? "private"
289320 pure $ GitHubSource
290321 { _gitHubSource_owner = owner
291322 , _gitHubSource_repo = repo
292323 , _gitHubSource_branch = branch
324+ , _gitHubSource_private = fromMaybe False private
293325 }
294326
295327parseGitSource :: Aeson.Object -> Aeson.Parser GitSource
@@ -297,10 +329,12 @@ parseGitSource v = do
297329 Just url <- parseGitUri <$> v Aeson..: "url"
298330 branch <- v Aeson..:! "branch"
299331 fetchSubmodules <- v Aeson..:! "fetchSubmodules"
332+ private <- v Aeson..:? "private"
300333 pure $ GitSource
301334 { _gitSource_url = url
302335 , _gitSource_branch = branch
303336 , _gitSource_fetchSubmodules = fromMaybe False fetchSubmodules
337+ , _gitSource_private = fromMaybe False private
304338 }
305339
306340overwriteThunk :: MonadObelisk m => FilePath -> ThunkPtr -> m ()
@@ -327,13 +361,15 @@ encodeThunkPtrData (ThunkPtr rev src) = case src of
327361 , ("branch" .=) <$> _gitHubSource_branch s
328362 , Just $ "rev" .= Ref.toHexString (_thunkRev_commit rev)
329363 , Just $ "sha256" .= _thunkRev_nixSha256 rev
364+ , Just $ "private" .= _gitHubSource_private s
330365 ]
331366 ThunkSource_Git s -> encodePretty' plainGitCfg $ Aeson.object $ catMaybes
332− [ Just $ "url" .= render (_gitSource_url s)
367+ [ Just $ "url" .= gitUriToText (_gitSource_url s)
333368 , Just $ "rev" .= Ref.toHexString (_thunkRev_commit rev)
334369 , ("branch" .=) <$> _gitSource_branch s
335370 , Just $ "sha256" .= _thunkRev_nixSha256 rev
336371 , Just $ "fetchSubmodules" .= _gitSource_fetchSubmodules s
372+ , Just $ "private" .= _gitSource_private s
337373 ]
338374 where
339375 githubCfg = defConfig
@@ -354,6 +390,7 @@ encodeThunkPtrData (ThunkPtr rev src) = case src of
354390 [ "url"
355391 , "rev"
356392 , "sha256"
393+ , "private"
357394 , "fetchSubmodules"
358395 ] <> compare
359396 , confTrailingNewline = True
@@ -377,8 +414,8 @@ createThunkWithLatest target s = do
377414 , _thunkPtr_rev = rev
378415 }
379416
380−updateThunkToLatest :: MonadObelisk m => FilePath -> Maybe String -> m ()
381−updateThunkToLatest target mBranch = withSpinner' ("Updating thunk " <> T.pack target <> " to latest") (pure $ const $ "Thunk " <> T.pack target <> " updated to latest") $ do
417+updateThunkToLatest :: MonadObelisk m => ThunkUpdateConfig -> FilePath -> m ()
418+updateThunkToLatest (ThunkUpdateConfig mBranch thunkConfig) target = withSpinner' ("Updating thunk " <> T.pack target <> " to latest") (pure $ const $ "Thunk " <> T.pack target <> " updated to latest") $ do
382419 checkThunkDirectory "ob thunk update directory cannot be '.'" target
383420 -- check to see if thunk should be updated to a specific branch or just update it's current branch
384421 case mBranch of
@@ -390,7 +427,7 @@ updateThunkToLatest target mBranch = withSpinner' ("Updating thunk " <> T.pack t
390427 ThunkData_Checkout _ -> failWith "cannot update an unpacked thunk"
391428 let src = _thunkPtr_source ptr
392429 rev <- getLatestRev src
393− overwriteThunk overwrite $ ThunkPtr
430+ overwriteThunk overwrite $ modifyThunkPtrByConfig thunkConfig $ ThunkPtr
394431 { _thunkPtr_source = src
395432 , _thunkPtr_rev = rev
396433 }
@@ -398,24 +435,25 @@ updateThunkToLatest target mBranch = withSpinner' ("Updating thunk " <> T.pack t
398435 Left err -> failWith $ T.pack $ "thunk update: " <> show err
399436 Right c -> case c of
400437 ThunkData_Packed t -> case _thunkPtr_source t of
401− ThunkSource_Git tsg -> setThunk target tsg branch
438+ ThunkSource_Git tsg -> setThunk thunkConfig target tsg branch
402439 ThunkSource_GitHub tsgh -> do
403440 let tsg = forgetGithub False tsgh
404− setThunk target tsg branch
405− ThunkData_Checkout _ -> failWith $ T.pack $ "thunk located at " <> (show target) <> " is unpacked. Use ob thunk pack on the desired directory and then try ob thunk update again."
441+ setThunk thunkConfig target tsg branch
442+ ThunkData_Checkout _ -> failWith $ T.pack $ "thunk located at " <> show target <> " is unpacked. Use ob thunk pack on the desired directory and then try ob thunk update again."
406443
407−setThunk :: MonadObelisk m => FilePath -> GitSource -> String -> m ()
408−setThunk target gs branch = do
444+setThunk :: MonadObelisk m => ThunkConfig -> FilePath -> GitSource -> String -> m ()
445+setThunk thunkConfig target gs branch = do
409446 newThunkPtr <- uriThunkPtr (_gitSource_url gs) (Just $ T.pack branch) Nothing
410447 overwriteThunk target newThunkPtr
411− updateThunkToLatest target Nothing
448+ updateThunkToLatest (ThunkUpdateConfig Nothing thunkConfig) target
412449
413450-- | All recognized github standalone loaders, ordered from newest to oldest.
414451-- This tool will only ever produce the newest one when it writes a thunk.
415452gitHubStandaloneLoaders :: NonEmpty Text
416453gitHubStandaloneLoaders =
417− gitHubStandaloneLoaderV2 :|
454+ gitHubStandaloneLoaderV4 :|
418455 [ gitHubStandaloneLoaderV3
456+ , gitHubStandaloneLoaderV2
419457 , gitHubStandaloneLoaderV1
420458 ]
421459
@@ -455,10 +493,23 @@ gitHubStandaloneLoaderV3 = T.unlines
455493 , "in import (fetch (builtins.fromJSON (builtins.readFile ./github.json)))"
456494 ]
457495
496+gitHubStandaloneLoaderV4 :: Text
497+gitHubStandaloneLoaderV4 = T.unlines
498+ [ "# DO NOT HAND-EDIT THIS FILE"
499+ , "let fetch = { private ? false, fetchSubmodules ? false, owner, repo, rev, sha256, ... }:"
500+ , " if !fetchSubmodules && !private then builtins.fetchTarball {"
501+ , " url = \"https://github.com/${owner}/${repo}/archive/${rev}.tar.gz\"; inherit sha256;"
502+ , " } else (import <nixpkgs> {}).fetchFromGitHub {"
503+ , " inherit owner repo rev sha256 fetchSubmodules private;"
504+ , " };"
505+ , "in import (fetch (builtins.fromJSON (builtins.readFile ./github.json)))"
506+ ]
507+
458508plainGitStandaloneLoaders :: NonEmpty Text
459509plainGitStandaloneLoaders =
460− plainGitStandaloneLoaderV1 :|
461− [ plainGitStandaloneLoaderV2
510+ plainGitStandaloneLoaderV3 :|
511+ [ plainGitStandaloneLoaderV1
512+ , plainGitStandaloneLoaderV2
462513 ]
463514
464515plainGitStandaloneLoaderV1 :: Text
@@ -479,6 +530,22 @@ plainGitStandaloneLoaderV2 = T.unlines
479530 , "in import (fetchGit (builtins.fromJSON (builtins.readFile ./git.json)))"
480531 ]
481532
533+plainGitStandaloneLoaderV3 :: Text
534+plainGitStandaloneLoaderV3 = T.unlines
535+ [ "# DO NOT HAND-EDIT THIS FILE"
536+ , "let fetch = {url, rev, ref ? null, sha256 ? null, fetchSubmodules ? false, private ? false, ...}:"
537+ , " let realUrl = let firstChar = builtins.substring 0 1 url; in"
538+ , " if firstChar == \"/\" then /. + url"
539+ , " else if firstChar == \".\" then ./. + url"
540+ , " else url;"
541+ , " in if !fetchSubmodules && private then builtins.fetchGit {"
542+ , " url = realUrl; inherit rev;"
543+ , " } else (import <nixpkgs> {}).fetchgit {"
544+ , " url = realUrl; inherit rev sha256;"
545+ , " };"
546+ , "in import (fetch (builtins.fromJSON (builtins.readFile ./git.json)))"
547+ ]
548+
482549thunkFileNames :: [FilePath]
483550thunkFileNames = ["default.nix", "github.json", "git.json"]
484551
@@ -520,7 +587,7 @@ nixBuildThunkAttrWithCache thunkDir attr = do
520587 Just c -> return c
521588 Nothing -> do
522589 putLog Warning $ T.pack $ mconcat [thunkDir, ": ", attr, " not cached, building ..."]
523− _ <- nixCmd $ NixCmd_Build$ def
590+ _ <- nixCmd $ NixCmd_Build $ def
524591 Lens.& nixBuildConfig_outLink Lens..~ OutLink_IndirectRoot cachePath
525592 Lens.& nixCmdConfig_target Lens..~ Target
526593 { _target_path = Just thunkDir
@@ -568,13 +635,13 @@ updateThunk p f = withSystemTempDirectory "obelisk-thunkptr-" $ \tmpDir -> do
568635 Right (ThunkData_Packed _) -> do
569636 let tmpThunk = tmpDir </> "thunk"
570637 callProcessAndLogOutput (Notice, Error) $
571− proc "cp" ["-r", "-T", thunkDir, tmpThunk]
638+ proc cp ["-r", "-T", thunkDir, tmpThunk]
572639 return tmpThunk
573− Right _ -> failWith $ "Thunk is not packed"
640+ Right _ -> failWith "Thunk is not packed"
574641 updateThunkFromTmp p' = do
575− _ <- packThunk' True False p'
642+ _ <- packThunk' True (ThunkPackConfig False (ThunkConfig Nothing)) p'
576643 callProcessAndLogOutput (Notice, Error) $
577− proc "cp" ["-r", "-T", p', p]
644+ proc cp ["-r", "-T", p', p]
578645
579646finalMsg :: Bool -> (a -> Text) -> Maybe (a -> Text)
580647finalMsg noTrail s = if noTrail then Nothing else Just s
@@ -606,7 +673,7 @@ unpackThunk' noTrail thunkDir = checkThunkDirectory "Can't pack/unpack from with
606673 let git = callProcessAndLogOutput (Notice, Notice) . gitProc tmpRepo
607674 git $ [ "clone" ]
608675 ++ ("--recursive" <$ guard (_gitSource_fetchSubmodules s))
609− ++ [ T.unpack $ render $ _gitSource_url s ]
676+ ++ [ T.unpack $ gitUriToText $ _gitSource_url s ]
610677 ++ do branch <- maybeToList $ _gitSource_branch s
611678 [ "--branch", T.unpack $ untagName branch ]
612679 git ["reset", "--hard", Ref.toHexString $ _thunkRev_commit $ _thunkPtr_rev tptr]
@@ -615,31 +682,39 @@ unpackThunk' noTrail thunkDir = checkThunkDirectory "Can't pack/unpack from with
615682
616683 liftIO $ createDirectory obGitDir
617684 callProcessAndLogOutput (Notice, Error) $
618− proc "cp" ["-r", "-T", thunkDir </> ".", obGitDir </> "orig-thunk"]
685+ proc cp ["-r", "-T", thunkDir </> ".", obGitDir </> "orig-thunk"]
619686 callProcessAndLogOutput (Notice, Error) $
620687 proc "rm" ["-r", thunkDir]
621688 callProcessAndLogOutput (Notice, Error) $
622689 proc "mv" ["-T", tmpRepo, thunkDir]
623690
624691--TODO: add a rollback mode to pack to the original thunk
625−packThunk :: MonadObelisk m => Bool -> FilePath -> m ThunkPtr
692+packThunk :: MonadObelisk m => ThunkPackConfig -> FilePath -> m ThunkPtr
626693packThunk = packThunk' False
627694
628−packThunk' :: MonadObelisk m => Bool -> Bool -> FilePath -> m ThunkPtr
629−packThunk' noTrail force thunkDir = checkThunkDirectory "Can't pack/unpack from within the thunk directory" thunkDir >> readThunk thunkDir >>= \case
695+packThunk' :: MonadObelisk m => Bool -> ThunkPackConfig -> FilePath -> m ThunkPtr
696+packThunk' noTrail (ThunkPackConfig force thunkConfig) thunkDir = checkThunkDirectory "Can't pack/unpack from within the thunk directory" thunkDir >> readThunk thunkDir >>= \case
630697 Left err -> failWith $ T.pack $ "thunk pack: " <> show err
631698 Right (ThunkData_Packed _) -> failWith "pack: thunk is already packed"
632699 Right (ThunkData_Checkout _) -> do
633700 withSpinner' ("Packing thunk " <> T.pack thunkDir)
634701 (finalMsg noTrail $ const $ "Packed thunk " <> T.pack thunkDir) $ do
635− thunkPtr <- getThunkPtr (not force) thunkDir
702+ thunkPtr <- modifyThunkPtrByConfig thunkConfig <$> getThunkPtr (not force) thunkDir
636703 callProcessAndLogOutput (Debug, Error) $ proc "rm" ["-rf", thunkDir]
637704 liftIO $ createThunk thunkDir thunkPtr
638705 pure thunkPtr
639706
707+modifyThunkPtrByConfig :: ThunkConfig -> ThunkPtr -> ThunkPtr
708+modifyThunkPtrByConfig (ThunkConfig markPrivate') ptr = case markPrivate' of
709+ Nothing -> ptr
710+ Just markPrivate -> ptr { _thunkPtr_source = case _thunkPtr_source ptr of
711+ ThunkSource_Git s -> ThunkSource_Git $ s { _gitSource_private = markPrivate }
712+ ThunkSource_GitHub s -> ThunkSource_GitHub $ s { _gitHubSource_private = markPrivate }
713+ }
714+
640715getThunkPtr :: forall m. MonadObelisk m => Bool -> FilePath -> m ThunkPtr
641716getThunkPtr checkClean thunkDir = do
642− when checkClean $ ensureCleanGitRepo thunkDir True $
717+ when checkClean $ ensureCleanGitRepo thunkDir True
643718 "thunk pack: thunk checkout contains unsaved modifications"
644719
645720 -- Check whether there are any stashes
@@ -661,7 +736,7 @@ getThunkPtr checkClean thunkDir = do
661736 <$> T.lines
662737 <$> readGitProcess thunkDir ["rev-parse", "HEAD"]
663738 case b of
664− (Just "HEAD") -> failWith $ T.unlines $
739+ (Just "HEAD") -> failWith $ T.unlines
665740 [ "thunk pack: You are in 'detached HEAD' state."
666741 , "If you want to pack at the current ref \
667742 \then please create a new branch with 'git checkout -b <new-branch-name>' and push this upstream."
@@ -733,7 +808,7 @@ getThunkPtr checkClean thunkDir = do
733808 [ "thunk pack: Certain branches in the thunk have commits not yet pushed upstream:"
734809 , ""
735810 ] ++
736− (flip map (Map.toList nonGood) $ \(branch, (upstream, (ahead, behind))) -> mconcat
811+ flip map (Map.toList nonGood) (\(branch, (upstream, (ahead, behind))) -> mconcat
737812 [" ", branch, " ahead: ", T.pack (show ahead), " behind: ", T.pack (show behind), " remote branch ", upstream]) ++
738813 [ ""
739814 , "Please push these upstream and try again. (Or just fetch, if they are somehow \
@@ -741,7 +816,7 @@ getThunkPtr checkClean thunkDir = do
741816 ]
742817
743818 -- We assume it's safe to pack the thunk at this point
744− putLog Informational $ "All changes safe in git remotes. OK to pack thunk."
819+ putLog Informational "All changes safe in git remotes. OK to pack thunk."
745820
746821 let remote = maybe "origin" snd $ flip Map.lookup headUpstream =<< mCurrentBranch
747822
@@ -773,7 +848,7 @@ getLatestRev os = do
773848-- performance. If that doesn't work (e.g. authentication issue), we fall back
774849-- on just doing things the normal way for git repos in general, and save it as
775850-- a regular git thunk.
776−uriThunkPtr :: MonadObelisk m => URI -> Maybe Text -> Maybe Text -> m ThunkPtr
851+uriThunkPtr :: MonadObelisk m => GitUri -> Maybe Text -> Maybe Text -> m ThunkPtr
777852uriThunkPtr uri mbranch mcommit = do
778853 commit <- case mcommit of
779854 Nothing -> gitGetCommitBranch uri mbranch >>= return . snd
@@ -784,7 +859,7 @@ uriThunkPtr uri mbranch mcommit = do
784859 case rev of
785860 Right r -> pure (ThunkSource_GitHub s, r)
786861 Left e -> do
787− putLog Warning $ "\
862+ putLog Warning "\
788863\Failed to fetch archive from GitHub. This is probably a private repo. \
789864\Falling back on normal fetchgit. Original failure:"
790865 errorToWarning e
@@ -801,33 +876,33 @@ uriThunkPtr uri mbranch mcommit = do
801876-- If the thunk is a GitHub thunk and fails, we do *not* fall back like with
802877-- `uriThunkPtr`. Unlike a plain URL, a thunk src explicitly states which method
803878-- should be employed, and so we respect that.
804−uriToThunkSource :: URI -> Maybe Text -> ThunkSource
805−uriToThunkSource u
806− | Right uriAuth <- uriAuthority u
807− , Just scheme <- unRText <$> uriScheme u
879+uriToThunkSource :: GitUri -> Maybe Text -> ThunkSource
880+uriToThunkSource (GitUri u)
881+ | Right uriAuth <- URI.uriAuthority u
882+ , Just scheme <- URI.unRText <$> URI.uriScheme u
808883 , case scheme of
809− "ssh" -> uriAuth == Authority
810− { authUserInfo = Just $ UserInfo (fromRight' $ mkUsername "git") Nothing
811− , authHost = fromRight' $ mkHost "github.com"
812− , authPort = Nothing
884+ "ssh" -> uriAuth == URI.Authority
885+ { URI.authUserInfo = Just $ URI.UserInfo (fromRight' $ URI.mkUsername "git") Nothing
886+ , URI.authHost = fromRight' $ URI.mkHost "github.com"
887+ , URI.authPort = Nothing
813888 }
814889 s -> s `L.elem` [ "git", "https", "http" ] -- "http:" just redirects to "https:"
815− && unRText (authHost uriAuth) == "github.com"
816− , Just (_, owner :| [repoish]) <- uriPath u
890+ && URI.unRText (URI.authHost uriAuth) == "github.com"
891+ , Just (_, owner :| [repoish]) <- URI.uriPath u
817892 = \mbranch -> ThunkSource_GitHub $ GitHubSource
818− { _gitHubSource_owner = N $ unRText owner
893+ { _gitHubSource_owner = N $ URI.unRText owner
819894 , _gitHubSource_repo = N $ let
820− repoish' = unRText repoish
821− in case T.stripSuffix ".git" repoish' of
822− Just repo -> repo
823− Nothing -> repoish'
895+ repoish' = URI.unRText repoish
896+ in fromMaybe repoish' $ T.stripSuffix ".git" repoish'
824897 , _gitHubSource_branch = N <$> mbranch
898+ , _gitHubSource_private = False -- TODO: Can we try something to infer this?
825899 }
826900
827901 | otherwise = \mbranch -> ThunkSource_Git $ GitSource
828− { _gitSource_url = u
902+ { _gitSource_url = GitUri u
829903 , _gitSource_branch = N <$> mbranch
830904 , _gitSource_fetchSubmodules = False -- TODO: How do we determine if this should be true?
905+ , _gitSource_private = False -- TODO: Can we try something to infer this?
831906 }
832907
833908-- Funny signature indicates no effects depend on the optional branch name.
@@ -840,19 +915,19 @@ githubThunkRev
840915githubThunkRev s commit = do
841916 owner <- forcePP $ _gitHubSource_owner s
842917 repo <- forcePP $ _gitHubSource_repo s
843− revTarball <- mkPathPiece $ commit <> ".tar.gz"
844− let archiveUri = URI
845− { uriScheme = Just $ fromRight' $ mkScheme "https"
846− , uriAuthority = Right $ Authority
847− { authUserInfo = Nothing
848− , authHost = fromRight' $ mkHost "github.com"
849− , authPort = Nothing
918+ revTarball <- URI.mkPathPiece $ commit <> ".tar.gz"
919+ let archiveUri = GitUri $ URI.URI
920+ { URI.uriScheme = Just $ fromRight' $ URI.mkScheme "https"
921+ , URI.uriAuthority = Right $ URI.Authority
922+ { URI.authUserInfo = Nothing
923+ , URI.authHost = fromRight' $ URI.mkHost "github.com"
924+ , URI.authPort = Nothing
850925 }
851− , uriPath = Just ( False
852− , owner :| [ repo, fromRight' $ mkPathPiece "archive", revTarball ]
926+ , URI.uriPath = Just ( False
927+ , owner :| [ repo, fromRight' $ URI.mkPathPiece "archive", revTarball ]
853928 )
854− , uriQuery = []
855− , uriFragment = Nothing
929+ , URI.uriQuery = []
930+ , URI.uriFragment = Nothing
856931 }
857932 hash <- getNixSha256ForUriUnpacked archiveUri
858933 putLog Debug $ "Nix sha256 is " <> hash
@@ -861,8 +936,8 @@ githubThunkRev s commit = do
861936 , _thunkRev_nixSha256 = hash
862937 }
863938 where
864− forcePP :: Name entity -> m (RText 'PathPiece)
865− forcePP = mkPathPiece . untagName
939+ forcePP :: Name entity -> m (URI.RText 'URI.PathPiece)
940+ forcePP = URI.mkPathPiece . untagName
866941
867942gitThunkRev
868943 :: MonadObelisk m
@@ -871,11 +946,11 @@ gitThunkRev
871946 -> m ThunkRev
872947gitThunkRev s commit = do
873948 let u = _gitSource_url s
874− protocols = ["https", "ssh", "git"]
875− Just scheme <- pure $ unRText <$> uriScheme u
949+ protocols = ["file", "https", "ssh", "git"]
950+ scheme = maybe "file" URI.unRText $ URI.uriScheme $ (\(GitUri x) -> x) u
876951 unless (T.toLower scheme `elem` protocols) $
877952 failWith $ "obelisk currently only supports "
878− <> T.intercalate ", " protocols <> " protocols for non-GitHub remotes"
953+ <> T.intercalate ", " protocols <> " protocols for plain Git remotes"
879954 hash <- nixPrefetchGit u commit $ _gitSource_fetchSubmodules s
880955 putLog Informational $ "Nix sha256 is " <> hash
881956 pure $ ThunkRev
@@ -889,10 +964,10 @@ gitThunkRev s commit = do
889964-- If the branch name is passed in, it is returned exactly as-is. If it is not
890965-- passed it, the default branch of the repo is used instead.
891966gitGetCommitBranch
892− :: MonadObelisk m => URI -> Maybe Text -> m (Text, CommitId)
967+ :: MonadObelisk m => GitUri -> Maybe Text -> m (Text, CommitId)
893968gitGetCommitBranch uri mbranch = withExitFailMessage ("Failure for git remote " <> uriMsg) $ do
894969 (_, bothMaps) <- gitLsRemote
895− (T.unpack $ render uri)
970+ (T.unpack $ gitUriToText uri)
896971 (GitRef_Branch <$> mbranch)
897972 Nothing
898973 branch <- case mbranch of
@@ -908,18 +983,21 @@ gitGetCommitBranch uri mbranch = withExitFailMessage ("Failure for git remote "
908983 pure (branch, commit)
909984 where
910985 rethrowE = either failWith pure
911− uriMsg = render uri
986+ uriMsg = gitUriToText uri
987+
988+parseGitUri :: Text -> Maybe GitUri
989+parseGitUri x = GitUri <$> (parseFileURI x <|> parseAbsoluteURI x <|> parseSshShorthand x)
912990
913−parseGitUri :: Text -> Maybe URI
914−parseGitUri x = parseAbsoluteURI x <|> parseSshShorthand x
991+parseFileURI :: Text -> Maybe URI.URI
992+parseFileURI uri = if "/" `T.isPrefixOf` uri then parseAbsoluteURI ("file://" <> uri) else Nothing
915993
916−parseAbsoluteURI :: Text -> Maybe URI
994+parseAbsoluteURI :: Text -> Maybe URI.URI
917995parseAbsoluteURI uri = do
918− parsedUri <- mkURI uri
919− guard $ isPathAbsolute parsedUri
920− pure $ parsedUri
996+ parsedUri <- URI.mkURI uri
997+ guard $ URI.isPathAbsolute parsedUri
998+ pure parsedUri
921999
922−parseSshShorthand :: Text -> Maybe URI
1000+parseSshShorthand :: Text -> Maybe URI.URI
9231001parseSshShorthand uri = do
9241002 -- This is what git does to check that the remote
9251003 -- is not a local file path when parsing shorthand.
@@ -932,4 +1010,4 @@ parseSshShorthand uri = do
9321010 -- This check is used to disambiguate a filepath containing a colon from shorthand
9331011 guard $ isNothing (T.findIndex (=='/') authAndHostname)
9341012 && not (T.null colonAndPath)
935− mkURI properUri
1013+ URI.mkURI properUri
lib/command/src/Obelisk/Command/Utils.hs+27−13
@@ -4,6 +4,7 @@
44{-# LANGUAGE ScopedTypeVariables #-}
55{-# LANGUAGE OverloadedStrings #-}
66{-# LANGUAGE PatternGuards #-}
7+{-# LANGUAGE TemplateHaskell #-}
78module Obelisk.Command.Utils where
89
910import Control.Applicative hiding (many)
@@ -23,7 +24,7 @@ import Data.Void (Void)
2324import System.Directory (canonicalizePath)
2425import System.Environment (getExecutablePath)
2526import System.Exit (ExitCode)
26−import qualified System.Process as P
27+import System.Which (staticWhich)
2728import Text.Megaparsec as MP
2829import Text.Megaparsec.Char as MP
2930
@@ -33,6 +34,9 @@ import Obelisk.CliApp
3334getObeliskExe :: IO FilePath
3435getObeliskExe = getExecutablePath >>= canonicalizePath
3536
37+cp :: FilePath
38+cp = $(staticWhich "cp")
39+
3640-- Check whether the working directory is clean
3741checkGitCleanStatus :: MonadObelisk m => FilePath -> Bool -> m Bool
3842checkGitCleanStatus repo withIgnored = do
@@ -61,20 +65,30 @@ initGit repo = do
6165 git ["add", "."]
6266 git ["commit", "-m", "Initial commit."]
6367
64−gitProcNoRepo :: [String] -> P.CreateProcess
65−gitProcNoRepo = P.proc "git"
68+gitProcNoRepo :: [String] -> ProcessSpec
69+gitProcNoRepo args = setEnvOverride (M.singleton "GIT_TERMINAL_PROMPT" "0" <>) $ proc "git" args
6670
67−gitProc :: FilePath -> [String] -> P.CreateProcess
71+gitProc :: FilePath -> [String] -> ProcessSpec
6872gitProc repo = gitProcNoRepo . runGitInDir
6973 where
7074 runGitInDir args' = case filter (not . null) args' of
7175 args@("clone":_) -> args <> [repo]
7276 args -> ["-C", repo] <> args
7377
74−-- | Recursively copy a directory using `cp -a`
75−copyDir :: FilePath -> FilePath -> P.CreateProcess
78+isolateGitProc :: ProcessSpec -> ProcessSpec
79+isolateGitProc = setEnvOverride (overrides <>)
80+ where
81+ overrides = M.fromList
82+ [ ("HOME", "/dev/null")
83+ , ("GIT_CONFIG_NOSYSTEM", "1")
84+ , ("GIT_TERMINAL_PROMPT", "0")
85+ , ("GIT_SSH_COMMAND", "ssh -o PreferredAuthentications=password -o PubkeyAuthentication=no -o GSSAPIAuthentication=no")
86+ ]
87+
88+-- | Recursively copy a directory using `cp -a` -- TODO: Should use -rT instead of -a
89+copyDir :: FilePath -> FilePath -> ProcessSpec
7690copyDir src dest =
77− (P.proc "cp" ["-a", ".", dest]) { P.cwd = Just src }
91+ setCwd (Just src) $ proc cp ["-a", ".", dest] -- TODO: This will break if dest is relative since we change cwd
7892
7993readGitProcess :: MonadObelisk m => FilePath -> [String] -> m Text
8094readGitProcess repo = readProcessAndLogOutput (Debug, Notice) . gitProc repo
@@ -87,15 +101,15 @@ processToShellString cmd args = unwords $ map quoteAndEscape (cmd : args)
87101 where quoteAndEscape x = T.unpack $ "'" <> T.replace "'" "'\''" (T.pack x) <> "'"
88102
89103-- | A simpler wrapper for CliApp's most used process function with sensible defaults.
90−runProc :: MonadObelisk m => P.CreateProcess -> m ()
104+runProc :: MonadObelisk m => ProcessSpec -> m ()
91105runProc = callProcessAndLogOutput (Notice, Error)
92106
93107-- | Like runProc, but all output goes to Debug logging level
94−runProcSilently :: MonadObelisk m => P.CreateProcess -> m ()
108+runProcSilently :: MonadObelisk m => ProcessSpec -> m ()
95109runProcSilently = callProcessAndLogOutput (Debug, Debug)
96110
97111-- | A simpler wrapper for CliApp's readProcessAndLogStderr with sensible defaults.
98−readProc :: MonadObelisk m => P.CreateProcess -> m Text
112+readProc :: MonadObelisk m => ProcessSpec -> m Text
99113readProc = readProcessAndLogOutput (Debug, Error)
100114
101115tshow :: Show a => a -> Text
@@ -129,13 +143,13 @@ gitLsRemote repository mRef mBranch = do
129143 (exitCode, out, _err) <- case mBranch of
130144 Nothing -> readCreateProcessWithExitCode $ gitProcNoRepo $
131145 ["ls-remote", "--exit-code", "--symref", repository]
132− ++ (maybeToList $ T.unpack . showGitRef <$> mRef)
133− Just branchName -> readCreateProcessWithExitCode $ gitProcNoRepo $
146+ ++ maybeToList (T.unpack . showGitRef <$> mRef)
147+ Just branchName -> readCreateProcessWithExitCode $ gitProcNoRepo
134148 ["ls-remote", "--exit-code", repository, branchName]
135149 let t = T.pack out
136150 maps <- case MP.runParser parseLsRemote "" t of
137151 Left err -> failWith $ T.pack $ MP.errorBundlePretty err
138− Right table -> pure $ bimap M.fromList M.fromList $ partitionEithers $ table
152+ Right table -> pure $ bimap M.fromList M.fromList $ partitionEithers table
139153 putLog Debug $ "git ls-remote maps: " <> T.pack (show maps)
140154 pure (exitCode, maps)
141155
lib/command/src/Obelisk/Command/VmBuilder.hs−1
@@ -18,7 +18,6 @@ import System.Directory (createDirectoryIfMissing)
1818import System.Exit (ExitCode(..))
1919import System.FilePath ((<.>), (</>))
2020import qualified System.Info
21−import System.Process (proc)
2221
2322import Obelisk.App (MonadObelisk, getObeliskUserStateDir)
2423import Obelisk.CliApp
lib/executable-config/default.nix+1−1
@@ -20,7 +20,7 @@ let injectConfig = config: assets: nixpkgs.runCommand "inject-config" {} (''
2020 '');
2121in with nixpkgs.haskell.lib; {
2222 haskellOverlay = self: super: {
23− obelisk-executable-config-lookup = self.callPackage (filterGitSource ./lookup) {};
23+ obelisk-executable-config-lookup = self.callCabal2nix "obelisk-executable-config-lookup" (filterGitSource ./lookup) {};
2424 };
2525 platforms = {
2626 android = {
lib/executable-config/lookup/default.nixdeleted−26
@@ -1,26 +0,0 @@
1−{ hostPlatform, ghc
2−, mkDerivation, base, base64-bytestring, bytestring, filepath, stdenv, text, transformers
3−, transformers-base, ref-tf, primitive, monad-control, reflex, reflex-dom
4−, jsaddle
5−, android-activity ? null, jsaddle-wkwebview ? null, ghcjs-dom ? null
6−}:
7−let isAndroid = hostPlatform.libc == "bionic";
8− isIOS = hostPlatform.isDarwin && hostPlatform.isAarch64;
9− isGhcjs = ghc.isGhcjs or false;
10−in mkDerivation {
11− pname = "obelisk-executable-config-lookup";
12− version = "0.1";
13− src = ./.;
14− libraryHaskellDepends = [
15− base bytestring filepath text transformers transformers-base ref-tf primitive monad-control reflex reflex-dom jsaddle
16− ] ++ (if isAndroid then [
17− android-activity
18− ] else if isIOS then [
19− jsaddle-wkwebview
20− ] else if isGhcjs then [
21− ghcjs-dom
22− ] else [
23− ]);
24− license = stdenv.lib.licenses.bsd3;
25−}
26−
lib/executable-config/lookup/obelisk-executable-config-lookup.cabal+1−1
@@ -27,7 +27,7 @@ library
2727 build-depends: directory
2828
2929 build-depends:
30− base,
30+ base,
3131 base64-bytestring,
3232 bytestring,
3333 containers,
lib/executable-config/lookup/src/Obelisk/Configs.hs+1
@@ -15,6 +15,7 @@ module Obelisk.Configs
1515 , ConfigsT
1616 , runConfigsT
1717 , mapConfigsT
18+ , getTextConfig
1819 ) where
1920
2021import Control.Applicative
lib/frontend/src/Obelisk/Frontend.hs+2−2
@@ -229,7 +229,7 @@ runFrontendWithConfigsAndCurrentRoute mode configs validFullEncoder frontend = d
229229renderFrontendHtml
230230 :: ( t ~ DomTimeline
231231 , MonadIO m
232− , widget ~ RoutedT t r (SetRouteT t r (RouteToUrlT r (ConfigsT (CookiesT (PostBuildT t (StaticDomBuilderT t (PerformEventT t DomHost)))))))
232+ , widget ~ RoutedT t r (SetRouteT t r (RouteToUrlT r (ConfigsT (CookiesT (HydratableT (PostBuildT t (StaticDomBuilderT t (PerformEventT t DomHost))))))))
233233 )
234234 => Map Text ByteString
235235 -> Cookies
@@ -241,7 +241,7 @@ renderFrontendHtml
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
244− html <- fmap snd $ liftIO $ renderStatic $ fmap fst $ runCookiesT cookies $ runConfigsT configs $ flip runRouteToUrlT urlEnc $ runSetRouteT $ flip runRoutedT (pure route) $
244+ html <- fmap snd $ liftIO $ renderStatic $ runHydratableT $ fmap fst $ runCookiesT cookies $ runConfigsT configs $ flip runRouteToUrlT urlEnc $ runSetRouteT $ flip runRoutedT (pure route) $
245245 el "html" $ do
246246 el "head" $ do
247247 baseTag
lib/route/src/Obelisk/Route.hs+48−1
@@ -21,6 +21,8 @@
2121module Obelisk.Route
2222 ( R
2323 , pattern (:/)
24+ , (:.)
25+ , pattern (:.)
2426 , hoistR
2527 , PageName
2628 , PathQuery
@@ -43,6 +45,9 @@ module Obelisk.Route
4345 , checkEnum1EncoderFunc
4446 , unitEncoder
4547 , pathOnlyEncoder
48+ , addPathSegmentEncoder
49+ , pathParamEncoder
50+ , pathLiteralEncoder
4651 , singletonListEncoder
4752 , unpackTextEncoder
4853 , prefixTextEncoder
@@ -200,6 +205,48 @@ mapSome f (Some a) = Some $ f a
200205hoistR :: (forall x. f x -> g x) -> R f -> R g
201206hoistR f (x :=> Identity y) = f x :/ y
202207
208+--------------------------------------------------------------------------------
209+-- Dealing with pairs (i.e. non-dependently-typed subroutes/paths)
210+--------------------------------------------------------------------------------
211+
212+infixr 5 :.
213+type (:.) = (,)
214+
215+{-# COMPLETE (:.) #-}
216+pattern (:.) :: a -> b -> a :. b
217+pattern a :. b = (a, b)
218+
219+addPathSegmentEncoder
220+ :: ( Applicative check
221+ , MonadError Text parse
222+ )
223+ => Encoder check parse (Text, PageName) PageName
224+addPathSegmentEncoder = unsafeMkEncoder $ EncoderImpl
225+ { _encoderImpl_encode = \(ph, (pt, q)) -> (ph : pt, q)
226+ , _encoderImpl_decode = \(p, q) -> case p of
227+ [] -> throwError "Expected a path segment"
228+ ph : pt -> pure (ph, (pt, q))
229+ }
230+
231+pathParamEncoder
232+ :: forall check parse item rest.
233+ ( Applicative check
234+ , MonadError Text parse
235+ )
236+ => Encoder check parse item Text
237+ -> Encoder check parse rest PageName
238+ -> Encoder check parse (item :. rest) PageName
239+pathParamEncoder itemUnchecked restUnchecked = addPathSegmentEncoder . bimap itemUnchecked restUnchecked
240+
241+pathLiteralEncoder
242+ :: ( Applicative check
243+ , MonadError Text parse
244+ )
245+ => Text
246+ -> Encoder check parse a PageName
247+ -> Encoder check parse a PageName
248+pathLiteralEncoder t e = addPathSegmentEncoder . bimap (unitEncoder t) e . coidl
249+
203250--------------------------------------------------------------------------------
204251-- Encoder fundamentals
205252--------------------------------------------------------------------------------
@@ -834,7 +881,7 @@ instance (GCompare br, GCompare fr) => GCompare (FullRoute br fr) where
834881 gcompare (FullRoute_Frontend x) (FullRoute_Frontend y) = gcompare x y
835882
836883instance (UniverseSome br, UniverseSome fr) => UniverseSome (FullRoute br fr) where
837− universeSome = [Some (FullRoute_Backend x) | Some x <- universeSome]
884+ universeSome = [Some (FullRoute_Backend x) | Some x <- universeSome]
838885 ++ [Some (FullRoute_Frontend x) | Some x <- universeSome]
839886
840887-- | Build the typical top level application route encoder from a route for handling 404's,
lib/route/src/Obelisk/Route/Frontend.hs+36−9
@@ -35,6 +35,8 @@ module Obelisk.Route.Frontend
3535 , mapRoutedT
3636 , subRoute
3737 , subRoute_
38+ , subPairRoute
39+ , subPairRoute_
3840 , maybeRoute
3941 , maybeRoute_
4042 , maybeRouted
@@ -51,6 +53,7 @@ module Obelisk.Route.Frontend
5153 , runRouteToUrlT
5254 , mapRouteToUrlT
5355 , routeLink
56+ , routeLinkDynAttr
5457 , dynRouteLink
5558 , adaptedUriPath
5659 , setAdaptedUriPath
@@ -72,11 +75,13 @@ import Control.Monad.Trans.Control
7275import Data.Coerce
7376import Data.Dependent.Sum (DSum (..))
7477import Data.GADT.Compare
78+import Data.Map (Map)
7579import Data.Monoid
7680import Data.Proxy
7781import Data.Text (Text)
7882import qualified Data.Text as T
7983import Data.Functor.Compose
84+import Data.Functor.Misc
8085import Reflex.Class
8186import Reflex.Host.Class
8287import Reflex.PostBuild.Class
@@ -202,10 +207,18 @@ subRoute_ :: (MonadFix m, MonadHold t m, GEq r, Adjustable t m) => (forall a. r
202207subRoute_ f = factorRouted $ strictDynWidget_ $ \(c :=> r') -> do
203208 runRoutedT (f c) r'
204209
210+-- | Like 'subRoute_', but with a pair rather than an R
211+subPairRoute_ :: (MonadFix m, MonadHold t m, Eq a, Adjustable t m) => (a -> RoutedT t b m ()) -> RoutedT t (a, b) m ()
212+subPairRoute_ f = withRoutedT (fmap (\(a, b) -> Const2 a :/ b)) $ subRoute_ (\(Const2 a) -> f a)
213+
205214subRoute :: (MonadFix m, MonadHold t m, GEq r, Adjustable t m) => (forall a. r a -> RoutedT t a m b) -> RoutedT t (R r) m (Dynamic t b)
206215subRoute f = factorRouted $ strictDynWidget $ \(c :=> r') -> do
207216 runRoutedT (f c) r'
208217
218+-- | Like 'subRoute_', but with a pair rather than an R
219+subPairRoute :: (MonadFix m, MonadHold t m, Eq a, Adjustable t m) => (a -> RoutedT t b m c) -> RoutedT t (a, b) m (Dynamic t c)
220+subPairRoute f = withRoutedT (fmap (\(a, b) -> Const2 a :/ b)) $ subRoute (\(Const2 a) -> f a)
221+
209222maybeRoute_ :: (MonadFix m, MonadHold t m, Adjustable t m) => m () -> RoutedT t r m () -> RoutedT t (Maybe r) m ()
210223maybeRoute_ n j = maybeRouted $ strictDynWidget_ $ \case
211224 Nothing -> n
@@ -500,10 +513,10 @@ runRouteViewT routeEncoder switchover useHash a = do
500513routeLink
501514 :: forall t m a route.
502515 ( DomBuilder t m
503− , RouteToUrl (R route) m
504− , SetRoute t (R route) m
516+ , RouteToUrl route m
517+ , SetRoute t route m
505518 )
506− => R route -- ^ Target route
519+ => route -- ^ Target route
507520 -> m a -- ^ Child widget
508521 -> m a
509522routeLink r w = do
@@ -515,9 +528,7 @@ routeLink r w = do
515528 setRoute $ r <$ domEvent Click e
516529 return a
517530
518−-- | A link widget that, when clicked, sets the route to current value of the
519−-- provided dynamic route. In non-javascript contexts the value of the dynamic post
520−-- build is used so the link still works like 'routeLink'.
531+-- | Like 'routeLinkDynAttr' but without custom attributes.
521532dynRouteLink
522533 :: forall t m a route.
523534 ( DomBuilder t m
@@ -526,11 +537,27 @@ dynRouteLink
526537 , SetRoute t (R route) m
527538 )
528539 => Dynamic t (R route) -- ^ Target route
529− -> m a -- ^ Child widget
540+ -> m a -- ^ Child widget of the @a@ element
541+ -> m a
542+dynRouteLink = routeLinkDynAttr mempty
543+
544+-- | An @a@-tag link widget that, when clicked, sets the route to current value of the
545+-- provided dynamic route. In non-JavaScript contexts the value of the dynamic post
546+-- build is used so the link still works like 'routeLink'.
547+routeLinkDynAttr
548+ :: forall t m a route.
549+ ( DomBuilder t m
550+ , PostBuild t m
551+ , RouteToUrl (R route) m
552+ , SetRoute t (R route) m
553+ )
554+ => Dynamic t (Map AttributeName Text) -- ^ Attributes for @a@ element. Note that if @href@ is present it will be ignored
555+ -> Dynamic t (R route) -- ^ Target route
556+ -> m a -- ^ Child widget of the @a@ element
530557 -> m a
531−dynRouteLink dr w = do
558+routeLinkDynAttr dAttr dr w = do
532559 enc <- askRouteToUrl
533− er <- dynamicAttributesToModifyAttributes $ ("href" =:) . enc <$> dr
560+ er <- dynamicAttributesToModifyAttributes $ zipDynWith (<>) (("href" =:) . enc <$> dr) dAttr
534561 let cfg = (def :: ElementConfig EventResult t (DomBuilderSpace m))
535562 & elementConfig_eventSpec %~ addEventSpecFlags (Proxy :: Proxy (DomBuilderSpace m)) Click (\_ -> preventDefault)
536563 & elementConfig_modifyAttributes .~ er
lib/run/obelisk-run.cabal+7
@@ -15,6 +15,7 @@ library
1515 , dependent-sum
1616 , dependent-sum-template
1717 , ghcjs-dom
18+ , HsOpenSSL
1819 , http-client
1920 , http-reverse-proxy
2021 , http-types
@@ -36,11 +37,17 @@ library
3637 , snap-core
3738 , streaming-commons
3839 , text
40+ , time
3941 , universe
42+ , utf8-string
4043 , wai
4144 , wai-websockets
4245 , warp
46+ , warp-tls
4347 , websockets
4448 exposed-modules:
4549 Obelisk.Run
4650 ghc-options: -Wall -fwarn-redundant-constraints
51+ if os(linux)
52+ cpp-options: -DIPROUTE_SUPPORTED
53+ build-depends: which
lib/run/src/Obelisk/Run.hs+56−13
@@ -1,3 +1,8 @@
1+{-# LANGUAGE CPP #-}
2+#if defined(IPROUTE_SUPPORTED)
3+{-# LANGUAGE TemplateHaskell #-}
4+#endif
5+
16{-# LANGUAGE ConstraintKinds #-}
27{-# LANGUAGE FlexibleContexts #-}
38{-# LANGUAGE FlexibleInstances #-}
@@ -23,6 +28,7 @@ import Data.ByteString (ByteString)
2328import qualified Data.ByteString as BS
2429import qualified Data.ByteString.Char8 as BSC
2530import qualified Data.ByteString.Lazy.Char8 as BSLC
31+import qualified Data.ByteString.UTF8 as BSUTF8
2632import Data.Functor.Identity
2733import Data.List (uncons)
2834import Data.Map (Map)
@@ -34,6 +40,7 @@ import Data.String (fromString)
3440import Data.Text (Text)
3541import qualified Data.Text as T
3642import qualified Data.Text.Encoding as T
43+import Data.Time.Clock (getCurrentTime, addUTCTime)
3744import Language.Javascript.JSaddle.Run (syncPoint)
3845import Language.Javascript.JSaddle.WebSockets
3946import Network.HTTP.Client (Manager, defaultManagerSettings, newManager)
@@ -43,6 +50,7 @@ import Network.Socket
4350import Network.Wai (Application)
4451import qualified Network.Wai as W
4552import Network.Wai.Handler.Warp
53+import Network.Wai.Handler.WarpTLS
4654import Network.Wai.Handler.Warp.Internal (settingsHost, settingsPort)
4755import Network.WebSockets (ConnectionOptions)
4856import Network.WebSockets.Connection (defaultConnectionOptions)
@@ -50,17 +58,24 @@ import qualified Obelisk.Asset.Serve.Snap as Snap
5058import Obelisk.Backend
5159import Obelisk.Frontend
5260import Obelisk.Route.Frontend
61+import qualified OpenSSL.PEM as PEM
62+import qualified OpenSSL.RSA as RSA
63+import qualified OpenSSL.X509 as X509
64+import qualified OpenSSL.X509.Request as X509Request
5365import Reflex.Dom.Core
5466import Snap.Core (Snap)
5567import System.Environment
5668import System.IO
5769import System.Process
58−import System.Exit (ExitCode(..))
5970import Text.URI (URI)
6071import qualified Text.URI as URI
6172import Text.URI.Lens
6273import Web.Cookie
6374
75+#if defined(IPROUTE_SUPPORTED)
76+import qualified System.Which
77+#endif
78+
6479run
6580 :: Int -- ^ Port to run the backend
6681 -> ([Text] -> Snap ()) -- ^ Static asset handler
@@ -92,7 +107,7 @@ runServeAsset rootPath = Snap.serveAsset "" rootPath . T.unpack . T.intercalate
92107
93108getConfigRoute :: Map Text ByteString -> Either Text URI
94109getConfigRoute configs = case Map.lookup "common/route" configs of
95− Just r ->
110+ Just r ->
96111 let stripped = T.strip (T.decodeUtf8 r)
97112 in case URI.mkURI stripped of
98113 Just route -> Right route
@@ -113,13 +128,36 @@ runWidget conf configs frontend validFullEncoder = do
113128 beforeMainLoop = do
114129 putStrLn $ "Frontend running on " <> T.unpack (URI.render uri)
115130 settings = setBeforeMainLoop beforeMainLoop (setPort port (setTimeout 3600 defaultSettings))
131+ -- Providing TLS here will also incidentally provide it to proxied requests to the backend.
132+ prepareRunner = case uri ^? uriScheme . _Just . unRText of
133+ Just "https" -> do
134+ -- Generate a private key and self-signed certificate for TLS
135+ privateKey <- RSA.generateRSAKey' 2048 3
136+
137+ certRequest <- X509Request.newX509Req
138+ _ <- X509Request.setPublicKey certRequest privateKey
139+ _ <- X509Request.signX509Req certRequest privateKey Nothing
140+
141+ cert <- X509.newX509 >>= X509Request.makeX509FromReq certRequest
142+ _ <- X509.setPublicKey cert privateKey
143+ now <- getCurrentTime
144+ _ <- X509.setNotBefore cert $ addUTCTime (-1) now
145+ _ <- X509.setNotAfter cert $ addUTCTime (365 * 24 * 60 * 60) now
146+ _ <- X509.signX509 cert privateKey Nothing
147+
148+ certByteString <- BSUTF8.fromString <$> PEM.writeX509 cert
149+ privateKeyByteString <- BSUTF8.fromString <$> PEM.writePKCS8PrivateKey privateKey Nothing
150+
151+ return $ runTLSSocket (tlsSettingsMemory certByteString privateKeyByteString)
152+ _ -> return runSettingsSocket
153+ runner <- prepareRunner
116154 bracket
117155 (bindPortTCPRetry settings (logPortBindErr port) (_runConfig_retryTimeout conf))
118156 close
119157 (\skt -> do
120158 man <- newManager defaultManagerSettings
121159 app <- obeliskApp configs defaultConnectionOptions frontend validFullEncoder uri $ fallbackProxy redirectHost redirectPort man
122− runSettingsSocket settings skt app)
160+ runner settings skt app)
123161
124162obeliskApp
125163 :: forall frontendRoute backendRoute
@@ -182,17 +220,22 @@ logPortBindErr p e = getProcessIdForPort p >>= \case
182220 Nothing -> putStrLn $ "runWidget: " <> show e
183221 Just pid -> putStrLn $ unwords [ "Port", show p, "is being used by process ID", show pid <> ".", "Please kill that process or change the port in config/common/route."]
184222
223+ssPath :: Maybe String
224+ssPath =
225+#if defined(IPROUTE_SUPPORTED)
226+ Just $(System.Which.staticWhich "ss")
227+#else
228+ Nothing
229+#endif
230+
185231getProcessIdForPort :: Int -> IO (Maybe Int)
186−getProcessIdForPort port = do
187− -- First check if 'ss' is available
188− (c, _, _) <- readProcessWithExitCode "which" ["ss"] mempty
189− case c of
190− ExitSuccess -> do
191− xs <- lines <$> readProcess "ss" ["-lptn", "sport = " <> show port] mempty
192− case uncons xs of
193− Just (_, x:_) -> return $ A.maybeResult $ A.parse parseSsPid $ BSC.pack x
194− _ -> return Nothing
195− _ -> return Nothing
232+getProcessIdForPort port = case ssPath of
233+ Just ss -> do
234+ xs <- lines <$> readProcess ss ["-lptn", "sport = " <> show port] mempty
235+ case uncons xs of
236+ Just (_, x:_) -> return $ A.maybeResult $ A.parse parseSsPid $ BSC.pack x
237+ _ -> return Nothing
238+ _ -> return Nothing
196239
197240parseSsPid :: A.Parser Int
198241parseSsPid = do
lib/selftest/obelisk-selftest.cabal+4−2
@@ -11,16 +11,18 @@ library
1111 base
1212 , containers
1313 , directory
14+ , filepath
1415 , hspec
1516 , http-client
1617 , http-types
1718 , HUnit
19+ , network
1820 , obelisk-cliapp
1921 , obelisk-executable-config-lookup
2022 , obelisk-run
23+ , which
2124 , process
22− , network
23− , shelly
25+ , shelly >= 1.9.0
2426 , temporary
2527 , text
2628 exposed-modules:
lib/selftest/src/Obelisk/SelfTest.hs+115−57
@@ -3,7 +3,8 @@
33{-# LANGUAGE OverloadedStrings #-}
44{-# LANGUAGE PatternGuards #-}
55{-# LANGUAGE ScopedTypeVariables #-}
6−{-# LANGUAGE TypeApplications #-}
6+{-# LANGUAGE ViewPatterns #-}
7+{-# LANGUAGE TemplateHaskell #-}
78module Obelisk.SelfTest where
89
910import Control.Exception (bracket, throw)
@@ -11,7 +12,7 @@ import Control.Monad
1112import Control.Monad.IO.Class
1213import Data.Bool (bool)
1314import Data.Function (fix)
14−import Data.Semigroup (Semigroup, (<>))
15+import Data.Semigroup ((<>))
1516import qualified Data.Set as Set
1617import Data.String
1718import Data.Text (Text)
@@ -21,11 +22,13 @@ import qualified Data.Text.IO as T
2122import qualified Network.HTTP.Client as HTTP
2223import qualified Network.HTTP.Types as HTTP
2324import qualified Network.Socket as Socket
24−import Shelly
25+import Shelly hiding (cp)
2526import System.Directory (withCurrentDirectory, getDirectoryContents)
2627import System.Environment
2728import System.Exit (ExitCode (..))
28−import System.Info
29+import System.FilePath (replaceBaseName, takeBaseName)
30+import System.Which (staticWhich)
31+import qualified System.Info
2932import System.IO (Handle, hClose)
3033import System.IO.Temp
3134import System.Process (readProcessWithExitCode, CreateProcess(cwd), readCreateProcessWithExitCode, proc)
@@ -43,16 +46,21 @@ data ObRunState
4346 | ObRunState_BackendStarted
4447 deriving (Eq, Show)
4548
46−doubleQuotes :: (IsString a, Semigroup a) => a -> a
47−doubleQuotes s = "\"" <> s <> "\""
49+cp :: FilePath
50+cp = $(staticWhich "cp")
51+
52+cabalPath :: FilePath
53+cabalPath = $(staticWhich "cabal")
54+
55+gitUserConfig :: [Text]
56+gitUserConfig = ["-c", "user.name=Obelisk Selftest", "-c", "user.email=noreply@example.com"]
4857
4958commit :: Text -> Sh ()
50−commit msg = void $ run "git"
51− [ "commit"
59+commit msg = void $ run "git" $ gitUserConfig <> [ "commit"
5260 , "--no-gpg-sign"
5361 , "--allow-empty"
5462 , "-m"
55− , doubleQuotes msg
63+ , msg
5664 ]
5765
5866-- TODO replace Void and stop using loosely typed synchronous exceptions
@@ -68,33 +76,64 @@ tshow = T.pack . show
6876shellyOb :: MonadIO m => (Sh a -> Sh a) -> Sh a -> m a
6977shellyOb f obTest = shelly $ f obTest
7078
79+
7180main :: IO ()
7281main = do
7382 -- Note: you can pass hspec arguments as well, eg: `-m <pattern>`
74− isVerbose <- (elem "-v") <$> getArgs
83+ isVerbose <- elem "-v" <$> getArgs
7584 unless isVerbose $
7685 putStrLn "Tests may take longer to run if there are unbuilt derivations: use -v for verbose output"
7786 let verbosity = bool silently verbosely isVerbose
7887 nixBuild args = run "nix-build" ("--no-out-link" : args)
79− obeliskImpl <- fromString <$> getEnv "OBELISK_IMPL"
88+ obeliskImplRaw <- fromString <$> getEnv "OBELISK_IMPL"
89+ let
90+ withObeliskImpl f =
91+ withSystemTempDirectory "obelisk-impl-copy" $ \(fromString -> obeliskImpl) -> do
92+ void . shellyOb verbosity $ chdir obeliskImpl $ do
93+ user <- T.strip <$> run "whoami" []
94+ run_ cp ["-rT", toTextIgnore obeliskImplRaw, toTextIgnore obeliskImpl]
95+ run_ "chown" ["-R", user, toTextIgnore obeliskImpl]
96+ run_ "chmod" ["-R", "g-rw,o-rw", toTextIgnore obeliskImpl]
97+ f obeliskImpl
98+
99+ withInitCache f obeliskImpl =
100+ withSystemTempDirectory "init Cache λ" $ \initCache -> do
101+ -- Setup the ob init cache
102+ void . shellyOb verbosity $ chdir initCache $ do
103+ run_ "ob" ["init", "--symlink", toTextIgnore obeliskImpl]
104+ run_ "git" ["init"]
105+
106+ f initCache
107+
108+ withObeliskImplAndInitCache f =
109+ withObeliskImpl $ \impl -> withInitCache (f impl) impl
110+
80111 httpManager <- HTTP.newManager HTTP.defaultManagerSettings
81− [p0, p1, p2, p3] <- liftIO $ getFreePorts 4
82− withSystemTempDirectory "initCache" $ \initCache -> do
83− -- Setup the ob init cache
84− void . shellyOb verbosity $ chdir (fromString initCache) $ do
85− run_ "ob" ["init"]
86− run_ "git" ["init"]
112+
113+ withObeliskImplAndInitCache $ \obeliskImpl initCache ->
87114 hspec $ parallel $ do
88115 let shelly_ = void . shellyOb verbosity
89116
90− inTmp :: (Shelly.FilePath -> Sh a) -> IO ()
91− inTmp f = withTmp (chdir <*> f)
117+ defaultTmpDirName = "test λ"
92118
93− withTmp f = shelly_ . withSystemTempDirectory "test" $ f . fromString
119+ inTmp' :: FilePath -> (FilePath -> Sh a) -> IO ()
120+ inTmp' dirname f = withTmp' dirname (chdir <*> f)
121+ inTmp = inTmp' defaultTmpDirName
94122
95− inTmpObInit f = inTmp $ \dir -> do
96− run_ "cp" ["-a", fromString $ initCache <> "/.", toTextIgnore dir]
123+ withTmp' dirname f = shelly_ . withSystemTempDirectory dirname $ f . fromString
124+ withTmp = withTmp' defaultTmpDirName
125+
126+ inTmpObInit' dirname f = inTmp' dirname $ \dir -> do
127+ run_ cp ["-rT", fromString initCache, toTextIgnore dir]
97128 f dir
129+ inTmpObInit = inTmpObInit' defaultTmpDirName
130+
131+ -- To be used in tests that change the obelisk impl directory
132+ inTmpObInitWithImplCopy f = inTmpObInit $ \dir ->
133+ withObeliskImpl $ \(fromString -> implCopy) -> do
134+ run_ "rm" [thunk]
135+ run_ "ln" ["-s", implCopy, thunk]
136+ f dir
98137
99138 assertRevEQ a b = liftIO . assertEqual "" "" =<< diff a b
100139 assertRevNE a b = liftIO . assertBool "" . (/= "") =<< diff a b
@@ -116,9 +155,9 @@ main = do
116155 describe "ob init" $ parallel $ do
117156 it "works with default impl" $ inTmp $ \_ -> run "ob" ["init"]
118157 it "works with master branch impl" $ inTmp $ \_ -> run "ob" ["init", "--branch", "master"]
119− it "works with symlink" $ inTmp $ \_ -> run "ob" ["init", "--symlink", obeliskImpl]
120− it "doesn't silently overwrite existing files" $ withSystemTempDirectory "ob-init" $ \dir -> do
121− let p force = (proc "ob" $ "--no-handoff" : "init" : if force then ["--force"] else []) { cwd = Just dir }
158+ it "works with symlink" $ inTmp $ \_ -> run "ob" ["init", "--symlink", toTextIgnore obeliskImpl]
159+ it "doesn't silently overwrite existing files" $ withSystemTempDirectory "ob-init λ" $ \dir -> do
160+ let p force = (System.Process.proc "ob" $ "--no-handoff" : "init" : ["--force"|force]) { cwd = Just dir }
122161 (ExitSuccess, _, _) <- readCreateProcessWithExitCode (p False) ""
123162 (ExitFailure _, _, _) <- readCreateProcessWithExitCode (p False) ""
124163 (ExitSuccess, _, _) <- readCreateProcessWithExitCode (p True) ""
@@ -128,51 +167,62 @@ main = do
128167 void $ errExit False $ run "ob" ["init", "--symlink", "/dev/null"]
129168 ls tmp >>= liftIO . assertEqual "" []
130169
131− it "produces a valid route config" $ inTmpObInit $ \tmp -> liftIO $ do
170+ it "produces a valid route config" $ inTmpObInit $ \tmp -> liftIO $
132171 withCurrentDirectory (T.unpack $ toTextIgnore tmp) $ do
133172 configs <- getConfigs
134173 return (either (const Nothing) Just $ getConfigRoute configs) `shouldNotReturn` Nothing
135174
136175 -- These tests fail with "Could not find module 'Obelisk.Generated.Static'"
137176 -- when not run by 'nix-build --attr selftest'
138− describe "ob run" $ parallel $ do
139− it "works in root directory" $ inTmpObInit $ \_ -> do
140− testObRunInDir p0 p1 Nothing httpManager
141− it "works in sub directory" $ inTmpObInit $ \_ -> do
142− testObRunInDir p2 p3 (Just "frontend") httpManager
177+ describe "ob run" $ {- NOT parallel $ -} do
178+ it "works in root directory" $ inTmpObInit $ \_ -> testObRunInDir Nothing httpManager
179+ it "works in sub directory" $ inTmpObInit $ \_ -> testObRunInDir (Just "frontend") httpManager
180+ it "works with differently named cabal files" $ inTmpObInit $ \_ -> do
181+ changeCabalPackageName "backend/backend.cabal" "new-backend"
182+ testObRunInDir Nothing httpManager
143183
144184 describe "obelisk project" $ parallel $ do
145− it "can build obelisk command" $ inTmpObInit $ \_ -> nixBuild ["-A", "command" , obeliskImpl]
146− it "can build obelisk skeleton" $ inTmpObInit $ \_ -> nixBuild ["-A", "skeleton", obeliskImpl]
147− it "can build obelisk shell" $ inTmpObInit $ \_ -> nixBuild ["-A", "shell", obeliskImpl]
148− it "can build everything" $ inTmpObInit $ \_ -> nixBuild [obeliskImpl]
185+ it "can build obelisk command" $ inTmpObInit $ \_ -> nixBuild ["-A", "command" , toTextIgnore obeliskImpl]
186+ it "can build obelisk skeleton" $ inTmpObInit $ \_ -> nixBuild ["-A", "skeleton", toTextIgnore obeliskImpl]
187+ it "can build obelisk shell" $ inTmpObInit $ \_ -> nixBuild ["-A", "shell", toTextIgnore obeliskImpl]
188+ it "can build everything" $ inTmpObInit $ \_ -> nixBuild [toTextIgnore obeliskImpl]
149189
150190 describe "blank initialized project" $ parallel $ do
151191
152− it "can build ghc.backend" $ inTmpObInit $ \_ -> do
153− nixBuild ["-A", "ghc.backend"]
154− it "can build ghcjs.frontend" $ inTmpObInit $ \_ -> do
155− nixBuild ["-A", "ghcjs.frontend"]
192+ it "can build ghc.backend" $ inTmpObInit $ \_ -> nixBuild ["-A", "ghc.backend"]
193+ it "can build ghcjs.frontend" $ inTmpObInit $ \_ -> nixBuild ["-A", "ghcjs.frontend"]
156194
157− if os == "darwin"
158− then it "can build ios" $ inTmpObInit $ \_ -> nixBuild ["-A", "ios.frontend"]
159− else it "can build android" $ inTmpObInit $ \_ -> nixBuild ["-A", "android.frontend"]
195+ if System.Info.os == "darwin"
196+ then it "can build ios" $ inTmpObInit $ \_ -> nixBuild ["-A", "ios.frontend"]
197+ else it "can build android after accepting license" $ inTmpObInit $ \dir -> do
198+ let defaultNixPath = dir </> ("default.nix" :: FilePath)
199+ writefile defaultNixPath
200+ =<< T.replace
201+ "# config.android_sdk.accept_license = false;"
202+ "config.android_sdk.accept_license = true;"
203+ <$> readfile defaultNixPath
204+ nixBuild ["-A", "android.frontend"]
160205
161206 forM_ ["ghc", "ghcjs"] $ \compiler -> do
162207 let
163− shell = "shells." <> compiler
164− inShell cmd' = run "nix-shell" ["default.nix", "-A", fromString shell, "--run", cmd']
165− it ("can enter " <> shell) $ inTmpObInit $ \_ -> inShell "exit"
166− it ("can build in " <> shell) $ inTmpObInit $ \_ -> inShell $ "cabal new-build --" <> fromString compiler <> " all"
167−
168− it "has idempotent thunk update" $ inTmpObInit $ \_ -> do
208+ shellName = "shells." <> compiler
209+ inShell cmd' = run_ "nix-shell" ["default.nix", "-A", fromString shellName, "--run", cmd']
210+ it ("can enter " <> shellName) $ inTmpObInit $ \_ -> inShell "exit"
211+ -- NOTE: We override the temporary directory name because cabal has a bug preventing new-build from working
212+ -- in a path that has unicode characters.
213+ it ("can build in " <> shellName) $ inTmpObInit' "test" $ \_ -> inShell $
214+ T.pack cabalPath <> " --version; " <> T.pack cabalPath <> " new-build --" <> T.pack compiler <> " all"
215+
216+ it "has idempotent thunk update" $ inTmpObInitWithImplCopy $ \_ -> do
217+ _ <- pack
169218 u <- update
170219 uu <- update
171220 assertRevEQ u uu
172221
173222 describe "ob thunk pack/unpack" $ parallel $ do
174− it "has thunk pack and unpack inverses" $ inTmpObInit $ \_ -> do
223+ it "has thunk pack and unpack inverses" $ inTmpObInitWithImplCopy $ \_ -> do
175224
225+ _ <- pack
176226 e <- commitAll
177227 eu <- unpack
178228 eup <- pack
@@ -191,7 +241,7 @@ main = do
191241 branch' <- chdir dir $ run "git" ["rev-parse", "--abbrev-ref", "HEAD"]
192242 liftIO $ assertEqual "" branch (T.strip branch')
193243
194− it "can pack and unpack plain git repos" $ do
244+ it "can pack and unpack plain git repos" $
195245 shelly_ $ withSystemTempDirectory "git-repo" $ \dir -> do
196246 let repo = toTextIgnore $ dir </> ("repo" :: String)
197247 run_ "git" ["clone", "https://github.com/haskell/process.git", repo]
@@ -210,7 +260,6 @@ main = do
210260 testThunkPack $ fromText repo
211261
212262 it "aborts thunk pack when there are uncommitted files" $ inTmpObInit $ \dir -> do
213− void $ unpack
214263 testThunkPack (dir </> thunk)
215264
216265 describe "ob thunk update --branch" $ parallel $ do
@@ -231,11 +280,12 @@ main = do
231280
232281
233282-- | Run `ob run` in the given directory (maximum of one level deep)
234−testObRunInDir :: Socket.PortNumber -> Socket.PortNumber -> Maybe Shelly.FilePath -> HTTP.Manager -> Sh ()
235−testObRunInDir p0 p1 mdir httpManager = handle_sh (\case ExitSuccess -> pure (); e -> throw e) $ do
283+testObRunInDir :: Maybe FilePath -> HTTP.Manager -> Sh ()
284+testObRunInDir mdir httpManager = handle_sh (\case ExitSuccess -> pure (); e -> throw e) $ do
285+ [p0, p1] <- liftIO $ getFreePorts 2
236286 let uri p = "http://localhost:" <> T.pack (show p) <> "/" -- trailing slash required for comparison
237287 writefile "config/common/route" $ uri p0
238− maybe id chdir mdir $ runHandle "ob" ["run"] $ \stdout -> do
288+ maybe id chdir mdir $ runHandle "ob" ["run", "-v"] $ \stdout -> do
239289 firstUri <- handleObRunStdout httpManager stdout
240290 let newUri = uri p1
241291 when (firstUri == newUri) $ errorExit $
@@ -246,7 +296,7 @@ testObRunInDir p0 p1 mdir httpManager = handle_sh (\case ExitSuccess -> pure ();
246296 then errorExit $ "Reloading failed: expected " <> newUri <> " but got " <> runningUri
247297 else exit 0
248298
249−testThunkPack :: Shelly.FilePath -> Sh ()
299+testThunkPack :: FilePath -> Sh ()
250300testThunkPack path' = withTempFile (T.unpack $ toTextIgnore path') "test-file" $ \file handle -> do
251301 let pack' = readProcessWithExitCode "ob" ["thunk", "pack", T.unpack $ toTextIgnore path'] ""
252302 ensureThunkPackFails q = liftIO $ pack' >>= \case
@@ -267,7 +317,7 @@ testThunkPack path' = withTempFile (T.unpack $ toTextIgnore path') "test-file" $
267317 liftIO $ T.hPutStrLn handle "test file" >> hClose handle
268318 ensureThunkPackFails "modified"
269319 -- Existing stashes
270− void $ git ["stash"]
320+ void $ git $ gitUserConfig <> [ "stash" ]
271321 ensureThunkPackFails "has stashes"
272322
273323-- | Blocks until a non-empty line is available
@@ -289,7 +339,7 @@ alterRouteTo uri stdout = do
289339
290340-- | Handle stdout of `ob run`: check that the frontend and backend servers are started correctly
291341handleObRunStdout :: HTTP.Manager -> Handle -> Sh Text
292−handleObRunStdout httpManager stdout = flip fix (ObRunState_Init, []) $ \loop (state, msgs) -> do
342+handleObRunStdout httpManager stdout = flip fix (ObRunState_Init, []) $ \loop (state, msgs) ->
293343 liftIO (T.hGetLine stdout) >>= \t -> case state of
294344 ObRunState_Init
295345 | "Running test..." `T.isPrefixOf` t -> loop (ObRunState_BackendStarted, msgs)
@@ -312,6 +362,14 @@ obRunCheck httpManager _stdout frontendUri = do
312362 req frontendUri >>= \r -> when (HTTP.responseStatus r /= HTTP.ok200) $ errorExit $
313363 "Request to frontend server failed: " <> T.pack (show r)
314364
365+-- | Rename a cabal file and do a really dumb, brittle search/replace in its content to update the name.
366+changeCabalPackageName :: FilePath -> Text -> Sh ()
367+changeCabalPackageName cabalFile newName = do
368+ contents <- readfile cabalFile
369+ writefile (replaceBaseName cabalFile (T.unpack newName)) $
370+ T.replace (" " <> T.pack (takeBaseName cabalFile)) (" " <> newName) contents -- WARNING: Super brittle
371+ rm cabalFile
372+
315373getFreePorts :: Int -> IO [Socket.PortNumber]
316374getFreePorts 0 = pure []
317375getFreePorts n = Socket.withSocketsDo $ do
lib/tabulation/src/Data/Tabulation.hs+1−1
@@ -8,7 +8,7 @@ import Control.Lens
88-- | This is a class for record types whose fields can be enumerated by an associated GADT. It's closely related to the concept of a representable functor, except without the functor part, and the fields are not all the same type.
99class HasFields a where
1010 type Field a :: * -> *
11−
11+
1212 fieldLens :: Field a x -> Lens' a x
1313 tabulateFieldsA :: Applicative f => (forall x. Field a x -> f x) -> f a
1414
release.nix+8−86
@@ -1,88 +1,10 @@
1−{ self-args ? {}
2−, local-self ? import ./. self-args
1+{
2+ supportedSystems ? [ "x86_64-linux" "x86_64-darwin" ]
33}:
4+rec {
5+ recurseForDerivations = true;
6+ build = import ./all-builds.nix { inherit supportedSystems; };
7+ test = import ./all-tests.nix { inherit supportedSystems; };
48
5−let
6− inherit (local-self.nixpkgs) lib runCommand nix;
7−
8− cacheBuildSystems = [ "x86_64-linux" "x86_64-darwin" ];
9−
10− obeliskPackagesCommon = [
11− "obelisk-frontend"
12− "obelisk-route"
13− "obelisk-executable-config-lookup"
14− ];
15−
16− obeliskPackagesBackend = obeliskPackagesCommon ++ [
17− "obelisk-asset-manifest"
18− "obelisk-asset-serve-snap"
19− "obelisk-backend"
20− "obelisk-cliapp"
21− "obelisk-command"
22− "obelisk-executable-config-inject"
23− "obelisk-frontend"
24− "obelisk-run"
25− "obelisk-route"
26− "obelisk-selftest"
27− "obelisk-snap-extras"
28− ];
29−
30− pnameToAttrs = pkgsSet: pnames:
31− lib.listToAttrs (map
32− (name: { inherit name; value = pkgsSet.${name}; })
33− pnames);
34−
35− concatDepends = let
36− extractDeps = x: (x.override {
37− mkDerivation = drv: {
38− out = builtins.concatLists [
39− (drv.buildDepends or [])
40− (drv.libraryHaskellDepends or [])
41− (drv.executableHaskellDepends or [])
42− ];
43− };
44− }).out;
45− in pkgAttrs: builtins.concatLists (map extractDeps (builtins.attrValues pkgAttrs));
46−
47− perPlatform = lib.genAttrs cacheBuildSystems (system: let
48− obelisk = import ./. { inherit system; };
49− reflex-platform = obelisk.reflex-platform;
50− ghc = pnameToAttrs
51− obelisk.haskellPackageSets.ghc
52− obeliskPackagesBackend;
53− ghcjs = pnameToAttrs
54− obelisk.haskellPackageSets.ghcjs
55− obeliskPackagesCommon;
56− cachePackages = builtins.concatLists [
57− (builtins.attrValues ghc)
58− (builtins.attrValues ghcjs)
59− (concatDepends ghc)
60− (concatDepends ghcjs)
61− [command serverExeSkeleton iosSkeleton androidSkeleton]
62− ];
63− command = local-self.command;
64− serverExeSkeleton = (import ./skeleton { obelisk = local-self; }).exe;
65− androidObelisk = import ./. (self-args // {
66− config.android_sdk.accept_license = true;
67− });
68− androidSkeleton = (import ./skeleton { obelisk = androidObelisk; }).android.frontend;
69− iosObelisk = import ./. (self-args // {
70− system = "x86_64-darwin";
71− iosSdkVersion = "10.2";
72− });
73− iosSkeleton = (import ./skeleton { obelisk = iosObelisk; }).ios.frontend;
74− in {
75− inherit
76− command
77− ghc ghcjs
78− serverExeSkeleton
79− iosSkeleton
80− androidSkeleton;
81− cache = reflex-platform.pinBuildInputs "obelisk-${system}" cachePackages;
82− });
83−
84− metaCache = local-self.pinBuildInputs
85− "obelisk-everywhere"
86− (map (a: a.cache) (builtins.attrValues perPlatform));
87−
88−in perPlatform // { inherit metaCache; }
9+ inherit (build) metaCache;
10+}
scripts/ghcid-this-lib+1−1
@@ -1,4 +1,4 @@
11#!/usr/bin/env bash
22set -euo pipefail
33
4−nix-shell -E "with (import ../.. {}); reflex-platform.workOn haskellPackageSets.ghc haskellPackageSets.ghc.obelisk-$(basename $PWD)" --run "cabal configure ; ghcid -c'cabal repl'"
4+nix-shell -E "with (import ../.. {}); reflex-platform.workOn haskellPackageSets.ghc haskellPackageSets.ghc.obelisk-$(basename $PWD)" --run "ghcid -c 'cabal new-repl'"
skeleton/.gitignore+1
@@ -1,5 +1,6 @@
11dist-newstyle
22result
3+result-*
34result-android
45result-ios
56result-exe
skeleton/common/common.cabal−3
@@ -9,9 +9,6 @@ library
99 , obelisk-route
1010 , mtl
1111 , text
12− default-extensions:
13− TypeFamilies
14− PolyKinds
1512 exposed-modules:
1613 Common.Api
1714 Common.Route
skeleton/common/src/Common/Route.hs+1
@@ -8,6 +8,7 @@
88{-# LANGUAGE OverloadedStrings #-}
99{-# LANGUAGE RankNTypes #-}
1010{-# LANGUAGE TemplateHaskell #-}
11+{-# LANGUAGE TypeFamilies #-}
1112module Common.Route where
1213
1314{- -- You will probably want these imports for composing Encoders.
skeleton/frontend/frontend.cabal+1
@@ -9,6 +9,7 @@ library
99 , common
1010 , obelisk-frontend
1111 , obelisk-route
12+ , jsaddle
1213 , reflex-dom
1314 , obelisk-executable-config-lookup
1415 , obelisk-generated-static
skeleton/frontend/src/Frontend.hs+22−4
@@ -1,30 +1,48 @@
11{-# LANGUAGE DataKinds #-}
22{-# LANGUAGE OverloadedStrings #-}
33{-# LANGUAGE TypeApplications #-}
4+
45module Frontend where
56
7+import Control.Monad
68import qualified Data.Text as T
79import qualified Data.Text.Encoding as T
10+import Language.Javascript.JSaddle (eval, liftJSM)
11+
812import Obelisk.Frontend
913import Obelisk.Configs
1014import Obelisk.Route
15+import Obelisk.Generated.Static
16+
1117import Reflex.Dom.Core
1218
1319import Common.Api
1420import Common.Route
15−import Obelisk.Generated.Static
1621
22+
23+-- This runs in a monad that can be run on the client or the server.
24+-- To run code in a pure client or pure server context, use one of the
25+-- `prerender` functions.
1726frontend :: Frontend (R FrontendRoute)
1827frontend = Frontend
19− { _frontend_head = el "title" $ text "Obelisk Minimal Example"
28+ { _frontend_head = do
29+ el "title" $ text "Obelisk Minimal Example"
30+ elAttr "link" ("href" =: static @"main.css" <> "type" =: "text/css" <> "rel" =: "stylesheet") blank
2031 , _frontend_body = do
21− text "Welcome to Obelisk!"
32+ el "h1" $ text "Welcome to Obelisk!"
2233 el "p" $ text $ T.pack commonStuff
34+
35+ -- `prerender` and `prerender_` let you choose a widget to run on the server
36+ -- during prerendering and a different widget to run on the client with
37+ -- JavaScript. The following will generate a `blank` widget on the server and
38+ -- print "Hello, World!" on the client.
39+ prerender_ blank $ liftJSM $ void $ eval ("console.log('Hello, World!')" :: T.Text)
40+
2341 elAttr "img" ("src" =: static @"obelisk.jpg") blank
2442 el "div" $ do
2543 exampleConfig <- getConfig "common/example"
2644 case exampleConfig of
2745 Nothing -> text "No config file found in config/common/example"
28− Just s -> text (T.decodeUtf8 s)
46+ Just s -> text $ T.decodeUtf8 s
2947 return ()
3048 }
skeleton/static/main.cssnew file+3
@@ -0,0 +1,3 @@
1+p {
2+ color: red;
3+}