on master help sign in
Merge remote-tracking branch 'origin/master' into cn-add-routeLinkAttr
commit5761260c5325f6394d76390d3d366c37927bc8d9 authorDivam <dfordivam@gmail.com> dateTue 28 Apr 2020, 19:04 (6 years ago) parent60d8ca1d9df151
28 files changed+1558−1012
ChangeLog.md+20−1 README.md+45−4 all-builds.nix+14−5 all-tests.nix+17−17 default.nix+16−92 dep/reflex-platform/github.json+3−3 guides/app-deploy/README.md+10−3 haskell-overlays/ghcide.nix+34 haskell-overlays/misc-deps.nix+20 haskell-overlays/obelisk.nix+43 haskell-overlays/tighten-ob-exes.nix+32 lib/cliapp/src/Obelisk/CliApp/Spinner.hs−1 lib/command/obelisk-command.cabal+3−3 lib/command/src/Obelisk/Command.hs+115−94 lib/command/src/Obelisk/Command/Deploy.hs+33−16 lib/command/src/Obelisk/Command/Nix.hs+4−4 lib/command/src/Obelisk/Command/Preprocessor.hs+18−19 lib/command/src/Obelisk/Command/Project.hs+43−28 lib/command/src/Obelisk/Command/Run.hs+226−74 lib/command/src/Obelisk/Command/Thunk.hs+396−277 lib/command/src/Obelisk/Command/Upgrade/Hash.hs−114 lib/command/src/Obelisk/Command/Utils.hs+17−2 lib/command/src/Obelisk/Command/VmBuilder.hs+2−1 lib/executable-config/default.nix+25−18 lib/route/src/Obelisk/Route.hs+7−5 lib/selftest/obelisk-selftest.cabal+2−1 lib/selftest/src/Obelisk/SelfTest.hs+398−230 nixpkgs-overlays/default.nix+15
ChangeLog.md+20−1
@@ -1,6 +1,25 @@
11# Revision history for obelisk
22
3−This project's release branch is `master`. This log is written from the perspective of the release branch: when changes hit `master`, they are considered released, and the date should reflect that release.
3+This project's release branch is `master`. This log is written from the perspective of the release branch: when changes hit `master`, they are considered released.
4+
5+## v0.8.0.0
6+
7+* ([#674](https://github.com/obsidiansystems/obelisk/pull/674), [#711](https://github.com/obsidiansystems/obelisk/pull/711)) Introduce a new thunk format to support accessing the thunk's source directly when packed. When packed, thunks have an additional file called `thunk.nix` and `default.nix` is now a think wrapper around that.
8+* ([#665](https://github.com/obsidiansystems/obelisk/pull/665)) Add `--interpret` and `--no-interpret` options to `ob run`/`ob watch`/`ob repl`/`ob shell`. These options allow you to pick which paths will be pre-compiled by `nix` when entering the shell/session and which won't. For example `ob run --no-interpret dep` will ensure that any dependencies found in `./dep` will be built by `nix` before loading the rest of the project into the `ghci` session. The same configuration for `ob shell` will ensure that those packages are built and available in the `ghc` package database inside the shell.
9+
10+ **NOTE:** `ob shell`'s default behavior is now different. By default it now behaves like `ob run`/`ob watch`/`ob repl` in that it does *not* pre-build any packages whose `.cabal` or `package.yaml` files are found in the project. To regain the previous behavior, use `ob shell --no-interpret . --interpret backend --interpret common --interpret frontend` from the project root.
11+* ([#695](https://github.com/obsidiansystems/obelisk/pull/695)) `ob deploy init` now requires that your obelisk project be a clean `git` checkout with pushed changes. It has always required that your obelisk project be a `git` repository, but it did not require that your local changes be committed and pushed. This new requirement is added to ensure users don't accidentally create a deployment pointing to an old version of the project.
12+* ([#705](https://github.com/obsidiansystems/obelisk/pull/705)) Add `Obelisk.Route.packTextEncoder` and generalize `Obelisk.Route.unpackTextEncoder` over any `Data.Text.Lens.IsText`.
13+* ([#712](https://github.com/obsidiansystems/obelisk/pull/712)) Update [`reflex-platform`](https://github.com/reflex-frp/reflex-platform) to version 0.5.3.0.
14+* ([#700](https://github.com/obsidiansystems/obelisk/pull/700)) Ensure `ob init` uses the thunk format of the target obelisk rather than the one currently running the init command. If a user had installed a version of obelisk with a new thunk format, a newly initialized skeleton of an older version would not be able to unpack it's own `.obelisk/impl`.
15+* ([#693](https://github.com/obsidiansystems/obelisk/pull/693)) Fix a bug where some packages in `.attr-cache` directories would be incorrectly picked up and used instead of the correct ones when using `ob run`/`ob watch`/`ob repl`.
16+* ([#709](https://github.com/obsidiansystems/obelisk/pull/709)) Fix a bug in obelisk's preprocessor causing it to incorrectly skip files in some circumstances.
17+* ([#663](https://github.com/obsidiansystems/obelisk/pull/663)) Add **experimental** support for [`ghcide`](https://github.com/digital-asset/ghcide). See the README for more information.
18+* ([#714](https://github.com/obsidiansystems/obelisk/pull/714)) Miscellaneous improvements to CLI help and logging.
19+
20+## v0.7.0.1
21+
22+* Fix the version number for `ob` the command-line tool. ([#679](https://github.com/obsidiansystems/obelisk/pull/679))
423
524## v0.7.0.0
625
README.md+45−4
@@ -10,7 +10,10 @@ Obelisk provides an easy way to develop and deploy your [Reflex](https://github.
1010- [Developing an Obelisk project](#developing-an-obelisk-project)
1111 - [Adding Packages](#adding-packages)
1212 - [Adding Package Overrides](#adding-package-overrides)
13− - [Running over https](#running-over-https)
13+ - [Running over HTTPS](#running-over-https)
14+ - [IDE Support](#ide-support)
15+ - [Terminal-based feedback](#terminal-based-feedback)
16+ - [Experimental `ghcide` support](#experimental-ghcide-support)
1417- [Deploying](#deploying)
1518 - [Locally](#locally)
1619 - [Default EC2 Deployment](#default-ec2-deployment)
@@ -169,9 +172,47 @@ project ./. ({ pkgs, ... }: {
169172
170173### Running over HTTPS
171174
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+To run your app locally over HTTPS, update the protocol in `config/common/route` to `https`, and then use `ob run` as normal.
176+
177+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.
178+
179+### IDE Support
180+
181+#### Terminal-based feedback
182+
183+Obelisk officially supports terminal-based feedback (akin to [`ghcid`](https://github.com/ndmitchell/ghcid)) in `ob run` and `ob watch`.
184+
185+#### **Experimental** `ghcide` support
186+
187+> **NOTE:** `ghcide` support is strictly **experimental** and **not officially supported**. There is no guarantee that it will work for you or that this support will be maintained over time.
188+
189+To try out the **experimental** [`ghcide`](https://github.com/digital-asset/ghcide) support:
190+
191+ * Install a `ghcide` plugin for your editor.
192+ * Add `__withGhcide = true;` to your Obelisk project's `default.nix` (inside the call to `project`). With this setting, `ob shell` will provide `ghcide` as well.
193+ * Add `hie-bios.sh` to the root of your project:
194+ ```shell
195+ cat > hie-bios.sh <<'EOF'
196+ #!/usr/bin/env bash
197+ source "$HOME/.bash_profile"; # NOTE: Some editors need help finding 'ob' and this assumes that `ob` is made available on your `$PATH` in `.bash_profile`
198+ ob internal export-ghci-configuration > "$HIE_BIOS_OUTPUT"
199+ EOF
200+ chmod +x hie-bios.sh
201+ ```
202+ * **Note:** In some projects you need to use `ob internal export-ghci-configuration --use-relative-paths` in this script instead. This is known to happen when your project contains symlinks to other packages.
203+ * Add `hie.yaml` to the root of your project:
204+ ```shell
205+ echo 'cradle: { bios: { program: hie-bios.sh } }' > hie.yaml
206+ ```
207+ * Test the configuration with `ob shell ghcide`. Assuming your project compiles and works with `ghcide` otherwise, this should succeed.
208+ * Start your editor in a way that it can access the appropriate `ghcide`:
209+ * This can sometimes be achieved with `ob shell 'start my editor'`.
210+ * If you can't easily start your editor in a way that it inherits your shell, you may need to customize how your `ghcide` plugin finds `ghcide` or create a custom `ghcide` replacement script that you install globally. For example, for Visual Studio Code on macOS adding a script called `ghcide` to your `PATH` like the one below will cause `ghcide` from each project's `ob shell` to be used:
211+ ```shell
212+ #!/usr/bin/env bash
213+ source "$HOME/.bash_profile" # NOTE: This assumes that `ob` is made available on your `$PATH` in `.bash_profile`
214+ ob shell "ghcide $@"
215+ ```
175216
176217## Deploying
177218
all-builds.nix+14−5
@@ -53,19 +53,28 @@ let
5353 obelisk.haskellPackageSets.ghcjs
5454 obeliskPackagesCommon;
5555 command = obelisk.command;
56− skeleton = import ./skeleton { inherit obelisk; };
57− serverSkeletonExe = skeleton.exe;
56+
57+ withSkeletonOptions = skel: options: (skel.passthru.__unstable__.self.extend (self: super: {
58+ userSettings = super.userSettings // options;
59+ })).project;
60+ rawSkeleton = import ./skeleton { inherit obelisk; };
61+ skeleton = withSkeletonOptions rawSkeleton {
62+ withHoogle = true; # cache the Hoogle database for the skeleton
63+ __withGhcide = true; # cache the ghcide build for the skeleton
64+ };
65+
66+ serverSkeletonExe = rawSkeleton.exe;
5867 # TODO fix nixpkgs so it doesn't try to run the result of haskell shells as setup hooks.
5968 serverSkeletonShell = local-self.nixpkgs.runCommand "shell-safe-for-dep" {} ''
6069 touch "$out"
6170 echo "return" >> "$out"
6271 cat "${skeleton.shells.ghc}" >> "$out"
6372 '';
64− androidSkeleton = (import ./skeleton { inherit obelisk; }).android.frontend;
65− iosSkeleton = (import ./skeleton { inherit obelisk; }).ios.frontend;
73+ androidSkeleton = skeleton.android.frontend;
74+ iosSkeleton = skeleton.ios.frontend;
6675 nameSuffix = if profiling then "profiled" else "unprofiled";
6776 packages = {
68− skeletonProfiledObRun = skeleton.__unstable__.profiledObRun;
77+ skeletonProfiledObRun = rawSkeleton.__unstable__.profiledObRun;
6978 inherit
7079 command
7180 serverSkeletonShell
all-tests.nix+17−17
@@ -63,15 +63,18 @@ in
6363 '';
6464 in ''
6565 start_all()
66− githost.wait_for_open_port("22")
6766
68− with subtest("test obelisk is installed"):
67+ with subtest("obelisk is installed and git is configured"):
6968 client.succeed("ob --help")
69+ client.succeed('git config --global user.email "you@example.com"')
70+ client.succeed('git config --global user.name "Your Name"')
71+
72+ githost.wait_for_open_port("22")
7073
71− with subtest("test the client can access the server via ssh"):
74+ with subtest("the client can access the server via ssh"):
7275 client.succeed("mkdir -p ~/.ssh/")
7376 client.succeed(
74− "cp ${privateKeyFile} ~/.ssh/id_rsa"
77+ "cp ${privateKeyFile} ~/.ssh/id_rsa"
7578 )
7679 client.succeed("chmod 600 ~/.ssh/id_rsa")
7780 client.wait_until_succeeds(
@@ -82,36 +85,35 @@ in
8285 )
8386 client.wait_until_succeeds("ssh githost true")
8487
85− with subtest("test a remote bare repo can be started"):
88+ with subtest("a remote bare repo can be started"):
8689 githost.succeed("mkdir -p ~/myorg/myapp.git")
8790 githost.succeed("cd ~/myorg/myapp.git && git init --bare")
8891
89− with subtest("test a git project can be configured with a remote using ssh"):
92+ with subtest("a git project can be configured with a remote using ssh"):
9093 client.succeed("mkdir -p ~/code/myapp")
9194 client.succeed("cd ~/code/myapp && git init")
9295 client.succeed(
9396 "cp ${thunkableSample} ~/code/myapp/default.nix"
9497 )
9598 client.succeed("cd ~/code/myapp && git add .")
96− client.succeed('git config --global user.email "you@example.com"')
97− client.succeed('git config --global user.name "Your Name"')
99+
98100 client.succeed('cd ~/code/myapp && git commit -m "Initial"')
99101 client.succeed(
100102 "cd ~/code/myapp && git remote add origin root@githost:/root/myorg/myapp.git"
101103 )
102104
103105
104− with subtest("test pushing code to the remote"):
106+ with subtest("pushing code to the remote"):
105107 client.succeed("cd ~/code/myapp && git push -u origin master")
106108 client.succeed("cd ~/code/myapp && git status")
107109
108− with subtest("test obelisk can pack"):
110+ with subtest("obelisk can pack"):
109111 client.succeed("ob -v thunk pack ~/code/myapp")
110− client.succeed("grep -qF 'git' ~/code/myapp/default.nix")
112+ client.succeed("grep -qF 'git.json' ~/code/myapp/thunk.nix")
111113 client.succeed("grep -qF 'myorg' ~/code/myapp/git.json")
112114 client.succeed("ob -v thunk unpack ~/code/myapp")
113115
114− with subtest("test obelisk can set the public / private flag"):
116+ with subtest("obelisk can set the public / private flag"):
115117 client.succeed("ob -v thunk pack ~/code/myapp --private")
116118 client.fail("""grep -qF '"private": practice' ~/code/myapp/git.json""")
117119 client.succeed("""grep -qF '"private": true' ~/code/myapp/git.json""")
@@ -122,22 +124,20 @@ in
122124 client.succeed("nix-build ~/code/myapp")
123125 client.succeed("ob -v thunk unpack ~/code/myapp")
124126
125− with subtest("test building an invalid thunk fails"):
127+ with subtest("building an invalid thunk fails"):
126128 client.succeed("cd ~/code/myapp && git checkout -b bad")
127129 client.succeed(
128130 "cp ${invalidThunkableSample} ~/code/myapp/default.nix"
129131 )
130132 client.succeed("cd ~/code/myapp && git add .")
131− client.succeed('git config --global user.email "you@example.com"')
132− client.succeed('git config --global user.name "Your Name"')
133133 client.succeed('cd ~/code/myapp && git commit -m "Bad commit"')
134134 client.succeed("cd ~/code/myapp && git push -u origin bad")
135135 client.succeed("ob -v thunk pack ~/code/myapp --public")
136− client.fail("nix-build ~/code/myapp")
136+ client.fail("nix-build ~/code/myapp")
137137 client.succeed("ob -v thunk unpack ~/code/myapp")
138138 client.succeed("cd ~/code/myapp && git checkout master")
139139
140− with subtest("test obelisk can detect private repos"):
140+ with subtest("obelisk can detect private repos"):
141141 client.succeed("ob -v thunk pack ~/code/myapp")
142142 client.succeed("""grep -qF '"private": true' ~/code/myapp/git.json""")
143143 client.succeed("ob -v thunk unpack ~/code/myapp")
default.nix+16−92
@@ -11,103 +11,18 @@ let
1111
1212 inherit (import dep/gitignore.nix { inherit (nixpkgs) lib; }) gitignoreSource;
1313
14− cleanSource = src:
15− # WARNING: The order of application here seems to matter a great deal to
16− # how quickly `ghcid` is able to reload changes. As a rule of thumb,
17− # always apply `gitignoreSource` first.
18− # See https://github.com/obsidiansystems/obelisk/pull/666 and related.
19− pkgs.lib.cleanSource (gitignoreSource src);
20−
21− commandRuntimeDeps = pkgs: with pkgs; [
22− coreutils
23− git
24− nix-prefetch-git
25− openssh
26− ];
27−
2814 getReflexPlatform = { system, enableLibraryProfiling ? profiling }: reflex-platform-func {
2915 inherit iosSdkVersion config system enableLibraryProfiling;
3016
3117 nixpkgsOverlays = [
32− (self: super: {
33− obeliskExecutableConfig = import ./lib/executable-config { nixpkgs = pkgs; filterGitSource = cleanSource; };
34− })
18+ (import ./nixpkgs-overlays)
3519 ];
3620
3721 haskellOverlays = [
38−
39− # Fix misc upstream packages
40− (self: super: let
41− pkgs = self.callPackage ({ pkgs }: pkgs) {};
42− haskellLib = pkgs.haskell.lib;
43− in {
44− hnix = haskellLib.dontCheck (haskellLib.doJailbreak (self.callCabal2nix "hnix" (hackGet dep/hnix) {}));
45− hnix-store-core = self.callHackage "hnix-store-core" "0.1.0.0" {};
46−
47− ghcid = self.callCabal2nix "ghcid" (hackGet ./dep/ghcid) {};
48− # Exports more internals
49− snap-core = haskellLib.dontCheck (self.callCabal2nix "snap-core" (hackGet ./dep/snap-core) {});
50− })
51−
22+ (import ./haskell-overlays/misc-deps.nix { inherit hackGet; })
5223 pkgs.obeliskExecutableConfig.haskellOverlay
53−
54− # Add obelisk packages
55− (self: super: let
56− pkgs = self.callPackage ({ pkgs }: pkgs) {};
57− onLinux = pkg: f: if pkgs.stdenv.isLinux then f pkg else pkg;
58− in {
59− shelly = self.callHackage "shelly" "1.9.0" {};
60−
61− obelisk-executable-config-inject = pkgs.obeliskExecutableConfig.platforms.web.inject self;
62−
63− obelisk-asset-manifest = self.callCabal2nix "obelisk-asset-manifest" (cleanSource ./lib/asset/manifest) {};
64− obelisk-asset-serve-snap = self.callCabal2nix "obelisk-asset-serve-snap" (cleanSource ./lib/asset/serve-snap) {};
65− obelisk-backend = self.callCabal2nix "obelisk-backend" (cleanSource ./lib/backend) {};
66− obelisk-cliapp = self.callCabal2nix "obelisk-cliapp" (cleanSource ./lib/cliapp) {};
67− obelisk-command = haskellLib.overrideCabal (self.callCabal2nix "obelisk-command" (cleanSource ./lib/command) {}) {
68− librarySystemDepends = [
69− pkgs.jre
70− pkgs.nix
71− (haskellLib.justStaticExecutables self.ghcid)
72− ];
73− };
74− obelisk-frontend = self.callCabal2nix "obelisk-frontend" (cleanSource ./lib/frontend) {};
75− obelisk-run = onLinux (self.callCabal2nix "obelisk-run" (cleanSource ./lib/run) {}) (pkg:
76− haskellLib.overrideCabal pkg (drv: { librarySystemDepends = [ pkgs.iproute ]; })
77− );
78− obelisk-route = self.callCabal2nix "obelisk-route" (cleanSource ./lib/route) {};
79− obelisk-selftest = haskellLib.overrideCabal (self.callCabal2nix "obelisk-selftest" (cleanSource ./lib/selftest) {}) {
80− librarySystemDepends = [
81− pkgs.cabal-install
82− pkgs.coreutils
83− pkgs.git
84− pkgs.nix
85− ];
86− };
87− obelisk-snap-extras = self.callCabal2nix "obelisk-snap-extras" (cleanSource ./lib/snap-extras) {};
88− tabulation = self.callCabal2nix "tabulation" (cleanSource ./lib/tabulation) {};
89− })
90−
91− (self: super: let
92− pkgs = self.callPackage ({ pkgs }: pkgs) {};
93− haskellLib = pkgs.haskell.lib;
94− in {
95− # Dynamic linking with split objects dramatically increases startup time (about
96− # 0.5 seconds on a decent machine with SSD), so we do `justStaticExecutables`.
97− obelisk-command = haskellLib.overrideCabal
98− (haskellLib.generateOptparseApplicativeCompletion "ob"
99− (haskellLib.justStaticExecutables super.obelisk-command))
100− (drv: {
101− buildTools = (drv.buildTools or []) ++ [ pkgs.buildPackages.makeWrapper ];
102− postFixup = ''
103− ${drv.postFixup or ""}
104− # Make `ob` reference its runtime dependencies.
105− wrapProgram "$out"/bin/ob --prefix PATH : ${pkgs.lib.makeBinPath (commandRuntimeDeps pkgs)}
106− '';
107− });
108− obelisk-selftest = haskellLib.justStaticExecutables super.obelisk-selftest;
109− })
110−
24+ (import ./haskell-overlays/obelisk.nix)
25+ (import ./haskell-overlays/tighten-ob-exes.nix)
11126 ];
11227 };
11328
@@ -130,7 +45,7 @@ in rec {
13045 obelisk = ghcObelisk;
13146 obeliskEnvs = pkgs.lib.filterAttrs (k: _: pkgs.lib.strings.hasPrefix "obelisk-" k) ghcObeliskEnvs;
13247 command = ghcObelisk.obelisk-command;
133− shell = pinBuildInputs "obelisk-shell" ([command] ++ commandRuntimeDeps pkgs);
48+ shell = pinBuildInputs "obelisk-shell" ([command] ++ command.commandRuntimeDeps);
13449
13550 selftest = pkgs.writeScript "selftest" ''
13651 #!${pkgs.runtimeShell}
@@ -292,13 +207,14 @@ in rec {
292207 , shellToolOverrides ? _: _: {}
293208 , withHoogle ? false # Setting this to `true` makes shell reloading far slower
294209 , __closureCompilerOptimizationLevel ? "ADVANCED" # Set this to `null` to skip the closure-compiler step
210+ , __withGhcide ? false
295211 }:
296212 let
297213 allConfig = nixpkgs.lib.makeExtensible (self: {
298214 base = base';
299215 inherit args;
300216 userSettings = {
301− inherit android ios packages overrides tools shellToolOverrides withHoogle __closureCompilerOptimizationLevel;
217+ inherit android ios packages overrides tools shellToolOverrides withHoogle __closureCompilerOptimizationLevel __withGhcide;
302218 staticFiles = if staticFiles == null then self.base + /static else staticFiles;
303219 };
304220 frontendName = "frontend";
@@ -355,9 +271,17 @@ in rec {
355271 self.frontendName
356272 ];
357273
274+ shellToolOverrides = lib.composeExtensions
275+ self.userSettings.shellToolOverrides
276+ (if self.userSettings.__withGhcide
277+ then (import ./haskell-overlays/ghcide.nix)
278+ else (_: _: {})
279+ );
280+
358281 project = reflexPlatformProject ({...}: self.projectConfig);
359282 projectConfig = {
360− inherit (self.userSettings) shellToolOverrides tools withHoogle;
283+ inherit (self) shellToolOverrides;
284+ inherit (self.userSettings) tools withHoogle;
361285 overrides = self.totalOverrides;
362286 packages = self.combinedPackages;
363287 shells = {
dep/reflex-platform/github.json+3−3
@@ -1,8 +1,8 @@
11{
22 "owner": "reflex-frp",
33 "repo": "reflex-platform",
4− "branch": "ts-bump-reflex-dom",
4+ "branch": "release/0.5.3.0",
55 "private": false,
6− "rev": "1f30528ba4c5efa6bd12a5fe477751b7d771a639",
7− "sha256": "16f411wm27mx19v1wgk0s2723crkcz5gm9i5y64mljzslspl8ydn"
6+ "rev": "9779ea3b4c87fdc971057c561262696d5e9b1847",
7+ "sha256": "0v87ilal9355xwz8y9m0zh14pm9c0f7pqch0854kkj92ybc5l62q"
88}
guides/app-deploy/README.md+10−3
@@ -232,7 +232,7 @@ xdg-open http://$VM_IP
232232If that fails, just get the URL by running
233233
234234```bash
235−echo http://$VM_IP`
235+echo http://$VM_IP
236236```
237237
238238and copy/paste that URL into your browser navigation input.
@@ -257,7 +257,7 @@ git commit -m"Accept license"
257257git push
258258```
259259
260−Make sure *USB debugging* is enabled in your Android device ([instructions here](https://developer.android.com/studio/debug/dev-options) and connect the device using USB. Be sure to confirm any security prompts on the device.
260+Make sure *USB debugging* is enabled in your Android device ([instructions here](https://developer.android.com/studio/debug/dev-options)) and connect the device using USB. Be sure to confirm any security prompts on the device.
261261
262262Now update your deployment and deploy to Android:
263263
@@ -267,7 +267,14 @@ ob deploy update
267267ob deploy test android -v
268268```
269269
270−This deployment will ask you to create a password (at least 6 characters long) and then ask you a series of questions. You can pick arbitrary answers. If the deployment fails, try using different USB ports on your computer and running `ob deploy test android -v` again. The USB cable you use can also make a difference.
270+This deployment will ask you to create a password (at least 6 characters long) and then ask you a series of questions. You can pick arbitrary answers. When you are presented with prompt like:
271+
272+```
273+Is CN=a, OU=b, O=c, L=d, ST=e, C=f correct?
274+ [no]:
275+```
276+
277+You can enter `yes` to continue. If the deployment fails, try using different USB ports on your computer and running `ob deploy test android -v` again. The USB cable you use can also make a difference.
271278
272279When connecting your Android device you may be asked to "Allow USB debugging". You need to allow it.
273280![](assets/android-confirm-usb-debugging.jpg)
haskell-overlays/ghcide.nixnew file+34
@@ -0,0 +1,34 @@
1+self: super:
2+ let
3+ pkgs = self.callPackage ({ pkgs }: pkgs) {};
4+ inherit (pkgs.haskell.lib) dontCheck;
5+ in {
6+ ghcide = pkgs.haskell.lib.justStaticExecutables (dontCheck ((self.override {
7+ overrides = self: super: {
8+ ghc-check = self.callHackageDirect {
9+ pkg = "ghc-check";
10+ ver = "0.1.0.3";
11+ sha256 = "038llbvryk5y27jbdpbshp0zw5lw1j6m7qk7vx1n96ykqdzkh649";
12+ } {};
13+ lsp-test = dontCheck (self.callHackage "lsp-test" "0.6.1.0" {});
14+ haddock-library = dontCheck (self.callHackage "haddock-library" "1.8.0" {});
15+ haskell-lsp = dontCheck (self.callHackage "haskell-lsp" "0.19.0.0" {});
16+ haskell-lsp-types = dontCheck (self.callHackage "haskell-lsp-types" "0.19.0.0" {});
17+ regex-posix = dontCheck (self.callHackage "regex-posix" "0.96.0.0" {});
18+ test-framework = dontCheck (self.callHackage "test-framework" "0.8.2.0" {});
19+ regex-base = dontCheck (self.callHackage "regex-base" "0.94.0.0" {});
20+ regex-tdfa = dontCheck (self.callHackage "regex-tdfa" "1.3.1.0" {});
21+ shake = dontCheck (self.callHackage "shake" "0.18.4" {});
22+ hie-bios = dontCheck (self.callHackageDirect {
23+ pkg = "hie-bios";
24+ ver = "0.4.0";
25+ sha256 = "19lpg9ymd9656cy17vna8wr1hvzfal94gpm2d3xpnw1d5qr37z7x";
26+ } {});
27+ };
28+ }).callCabal2nix "ghcide" (pkgs.fetchFromGitHub {
29+ owner = "digital-asset";
30+ repo = "ghcide";
31+ rev = "v0.1.0";
32+ sha256 = "1kf71iix46hvyxviimrcv7kvsj67hcnnqlpdsmazmlmybf7wbqbb";
33+ }) {}));
34+}
haskell-overlays/misc-deps.nixnew file+20
@@ -0,0 +1,20 @@
1+{ hackGet }:
2+
3+# Fix misc upstream packages
4+self: super:
5+
6+let
7+ pkgs = self.callPackage ({ pkgs }: pkgs) {};
8+ haskellLib = pkgs.haskell.lib;
9+in
10+
11+{
12+ hnix = haskellLib.dontCheck (haskellLib.doJailbreak (self.callCabal2nix "hnix" (hackGet ../dep/hnix) {}));
13+ hnix-store-core = self.callHackage "hnix-store-core" "0.1.0.0" {};
14+
15+ ghcid = self.callCabal2nix "ghcid" (hackGet ../dep/ghcid) {};
16+ # Exports more internals
17+ snap-core = haskellLib.dontCheck (self.callCabal2nix "snap-core" (hackGet ../dep/snap-core) {});
18+
19+ shelly = self.callHackage "shelly" "1.9.0" {};
20+}
haskell-overlays/obelisk.nixnew file+43
@@ -0,0 +1,43 @@
1+# Add obelisk packages
2+
3+self: super:
4+
5+let
6+ pkgs = self.callPackage ({ pkgs }: pkgs) {};
7+ inherit (pkgs) obeliskCleanSource;
8+ haskellLib = pkgs.haskell.lib;
9+ onLinux = pkg: f: if pkgs.stdenv.isLinux then f pkg else pkg;
10+in
11+
12+{
13+ obelisk-executable-config-inject = pkgs.obeliskExecutableConfig.platforms.web.inject self;
14+
15+ obelisk-asset-manifest = self.callCabal2nix "obelisk-asset-manifest" (obeliskCleanSource ../lib/asset/manifest) {};
16+ obelisk-asset-serve-snap = self.callCabal2nix "obelisk-asset-serve-snap" (obeliskCleanSource ../lib/asset/serve-snap) {};
17+ obelisk-backend = self.callCabal2nix "obelisk-backend" (obeliskCleanSource ../lib/backend) {};
18+ obelisk-cliapp = self.callCabal2nix "obelisk-cliapp" (obeliskCleanSource ../lib/cliapp) {};
19+ obelisk-command = haskellLib.overrideCabal (self.callCabal2nix "obelisk-command" (obeliskCleanSource ../lib/command) {}) {
20+ librarySystemDepends = [
21+ pkgs.jre
22+ pkgs.nix
23+ (haskellLib.justStaticExecutables self.ghcid)
24+ ];
25+ };
26+ obelisk-frontend = self.callCabal2nix "obelisk-frontend" (obeliskCleanSource ../lib/frontend) {};
27+ obelisk-run = onLinux (self.callCabal2nix "obelisk-run" (obeliskCleanSource ../lib/run) {}) (pkg:
28+ haskellLib.overrideCabal pkg (drv: { librarySystemDepends = [ pkgs.iproute ]; })
29+ );
30+ obelisk-route = self.callCabal2nix "obelisk-route" (obeliskCleanSource ../lib/route) {};
31+ obelisk-selftest = haskellLib.overrideCabal (self.callCabal2nix "obelisk-selftest" (obeliskCleanSource ../lib/selftest) {}) {
32+ librarySystemDepends = [
33+ pkgs.cabal-install
34+ pkgs.coreutils
35+ pkgs.git
36+ pkgs.nix
37+ pkgs.nix-prefetch-git
38+ pkgs.rsync
39+ ];
40+ };
41+ obelisk-snap-extras = self.callCabal2nix "obelisk-snap-extras" (obeliskCleanSource ../lib/snap-extras) {};
42+ tabulation = self.callCabal2nix "tabulation" (obeliskCleanSource ../lib/tabulation) {};
43+}
haskell-overlays/tighten-ob-exes.nixnew file+32
@@ -0,0 +1,32 @@
1+self: super:
2+
3+let
4+ pkgs = self.callPackage ({ pkgs }: pkgs) {};
5+ haskellLib = pkgs.haskell.lib;
6+
7+ commandRuntimeDeps = with pkgs; [
8+ coreutils
9+ git
10+ nix-prefetch-git
11+ openssh
12+ ];
13+in
14+
15+{
16+ # Dynamic linking with split objects dramatically increases startup time (about
17+ # 0.5 seconds on a decent machine with SSD), so we do `justStaticExecutables`.
18+ obelisk-command = haskellLib.overrideCabal
19+ (haskellLib.generateOptparseApplicativeCompletion "ob"
20+ (haskellLib.justStaticExecutables super.obelisk-command))
21+ (drv: {
22+ buildTools = (drv.buildTools or []) ++ [ pkgs.buildPackages.makeWrapper ];
23+ postFixup = ''
24+ ${drv.postFixup or ""}
25+ # Make `ob` reference its runtime dependencies.
26+ wrapProgram "$out"/bin/ob --prefix PATH : ${pkgs.lib.makeBinPath commandRuntimeDeps}
27+ '';
28+ passthru = { inherit commandRuntimeDeps; };
29+ });
30+
31+ obelisk-selftest = haskellLib.justStaticExecutables super.obelisk-selftest;
32+}
lib/cliapp/src/Obelisk/CliApp/Spinner.hs−1
@@ -3,7 +3,6 @@
33{-# LANGUAGE MultiParamTypeClasses #-}
44{-# LANGUAGE OverloadedStrings #-}
55{-# LANGUAGE ScopedTypeVariables #-}
6−{-# LANGUAGE TupleSections #-}
76-- | Provides a simple CLI spinner that interoperates cleanly with the rest of the logging output.
87module Obelisk.CliApp.Spinner
98 ( withSpinner
lib/command/obelisk-command.cabal+3−3
@@ -1,5 +1,5 @@
11name: obelisk-command
2−version: 0.6
2+version: 0.8.0.0
33cabal-version: >= 1.8
44build-type: Simple
55
@@ -10,8 +10,6 @@ library
1010 , aeson-pretty
1111 , ansi-terminal
1212 , base
13− , base16-bytestring
14− , binary
1513 , bytestring
1614 , Cabal
1715 , containers
@@ -20,6 +18,7 @@ library
2018 , either
2119 , errors
2220 , exceptions
21+ , extra
2322 , filepath
2423 , git
2524 , github
@@ -32,6 +31,7 @@ library
3231 , megaparsec
3332 , modern-uri
3433 , monad-loops
34+ , monoidal-containers
3535 , mtl
3636 , network
3737 , network-uri
lib/command/src/Obelisk/Command.hs+115−94
@@ -3,20 +3,18 @@
33{-# LANGUAGE MultiParamTypeClasses #-}
44{-# LANGUAGE OverloadedStrings #-}
55{-# LANGUAGE ScopedTypeVariables #-}
6+{-# LANGUAGE TupleSections #-}
67module Obelisk.Command where
78
8−import Control.Monad
9−import Control.Monad.IO.Class (liftIO)
10−import qualified Data.Binary as Binary
9+import Control.Monad.IO.Class (MonadIO, liftIO)
1110import Data.Bool (bool)
12−import qualified Data.ByteString.Base16 as Base16
13−import qualified Data.ByteString.Lazy as LBS
1411import Data.Foldable (for_)
15−import Data.List
12+import Data.List (isInfixOf, isPrefixOf)
13+import Data.List.NonEmpty (NonEmpty, nonEmpty)
14+import qualified Data.List.NonEmpty as NonEmpty
15+import qualified Data.Map as Map
1616import qualified Data.Text as T
17−import Data.Text.Encoding
18−import Data.Text.Encoding.Error (lenientDecode)
19−import GHC.StaticPtr
17+import Data.Traversable (for)
2018import Options.Applicative
2119import Options.Applicative.Help.Pretty (text, (<$$>))
2220import System.Directory
@@ -76,8 +74,8 @@ argsInfo cfg = info (args cfg <**> helper) $ mconcat
7674initSource :: Parser InitSource
7775initSource = foldl1 (<|>)
7876 [ pure InitSource_Default
79− , InitSource_Branch <$> strOption (long "branch" <> metavar "BRANCH")
80− , InitSource_Symlink <$> strOption (long "symlink" <> action "directory" <> metavar "PATH")
77+ , InitSource_Branch <$> strOption (long "branch" <> metavar "BRANCH" <> help "Initialize the project using the given BRANCH of Obelisk's official repository")
78+ , InitSource_Symlink <$> strOption (long "symlink" <> action "directory" <> metavar "PATH" <> help "(Use with caution) Initialize the project using the copy of Obelisk found at the given PATH")
8179 ]
8280
8381initForce :: Parser Bool
@@ -86,11 +84,11 @@ initForce = switch (long "force" <> help "Allow ob init to overwrite files")
8684data ObCommand
8785 = ObCommand_Init InitSource Bool
8886 | ObCommand_Deploy DeployCommand
89− | ObCommand_Run
87+ | ObCommand_Run [(FilePath, Interpret)]
9088 | ObCommand_Profile String [String]
91− | ObCommand_Thunk ThunkCommand
92− | ObCommand_Repl
93− | ObCommand_Watch
89+ | ObCommand_Thunk ThunkOption
90+ | ObCommand_Repl [(FilePath, Interpret)]
91+ | ObCommand_Watch [(FilePath, Interpret)]
9492 | ObCommand_Shell ShellOpts
9593 | ObCommand_Doc String [String] -- shell and list of packages
9694 | ObCommand_Hoogle String Int -- shell and port
@@ -101,6 +99,9 @@ data ObInternal
10199 -- the preprocessor argument syntax is also handled outside
102100 -- optparse-applicative, but it shouldn't ever conflict with another syntax
103101 = ObInternal_ApplyPackages String String String [String]
102+ | ObInternal_ExportGhciConfig
103+ [(FilePath, Interpret)]
104+ Bool -- ^ Use relative paths
104105 deriving Show
105106
106107obCommand :: ArgsConfig -> Parser ObCommand
@@ -108,11 +109,11 @@ obCommand cfg = hsubparser
108109 (mconcat
109110 [ command "init" $ info (ObCommand_Init <$> initSource <*> initForce) $ progDesc "Initialize an Obelisk project"
110111 , command "deploy" $ info (ObCommand_Deploy <$> deployCommand cfg) $ progDesc "Prepare a deployment for an Obelisk project"
111− , command "run" $ info (pure ObCommand_Run) $ progDesc "Run current project in development mode"
112+ , command "run" $ info (ObCommand_Run <$> interpretOpts) $ progDesc "Run current project in development mode"
112113 , command "profile" $ info (uncurry ObCommand_Profile <$> profileCommand) $ progDesc "Run current project with profiling enabled"
113− , command "thunk" $ info (ObCommand_Thunk <$> thunkCommand) $ progDesc "Manipulate thunk directories"
114− , command "repl" $ info (pure ObCommand_Repl) $ progDesc "Open an interactive interpreter"
115− , command "watch" $ info (pure ObCommand_Watch) $ progDesc "Watch current project for errors and warnings"
114+ , command "thunk" $ info (ObCommand_Thunk <$> thunkOption) $ progDesc "Manipulate thunk directories"
115+ , command "repl" $ info (ObCommand_Repl <$> interpretOpts) $ progDesc "Open an interactive interpreter"
116+ , command "watch" $ info (ObCommand_Watch <$> interpretOpts) $ progDesc "Watch current project for errors and warnings"
116117 , command "shell" $ info (ObCommand_Shell <$> shellOpts) $ progDesc "Enter a shell with project dependencies"
117118 , command "doc" $ info (ObCommand_Doc <$> shellFlags <*> packageNames) $
118119 progDesc "List paths to haddock documentation for specified packages"
@@ -120,8 +121,17 @@ obCommand cfg = hsubparser
120121 text "Hint: To open the documentation you can pipe the output of this command like"
121122 <$$> text "ob doc reflex reflex-dom-core | xargs -n1 xdg-open")
122123 , command "hoogle" $ info (ObCommand_Hoogle <$> shellFlags <*> portOpt 8080) $ progDesc "Run a hoogle server locally for your project's dependency tree"
124+ , command "internal" $ info (ObCommand_Internal <$> internalCommand) $ progDesc "Internal Obelisk commands with unstable APIs"
123125 ])
124126
127+internalCommand :: Parser ObInternal
128+internalCommand = hsubparser $ mconcat
129+ [ command "export-ghci-configuration" $ info (ObInternal_ExportGhciConfig <$> interpretOpts <*> useRelativePathsFlag)
130+ $ progDesc "Export the GHCi configuration used by ob run, etc.; useful for IDE integration"
131+ ]
132+ where
133+ useRelativePathsFlag = switch (long "use-relative-paths" <> help "Use relative paths")
134+
125135packageNames :: Parser [String]
126136packageNames = some (strArgument (metavar "PACKAGE-NAME..."))
127137
@@ -175,16 +185,6 @@ data DeployCommand
175185 | DeployCommand_Update
176186 deriving Show
177187
178−data DeployInitOpts = DeployInitOpts
179− { _deployInitOpts_outputDir :: FilePath
180− , _deployInitOpts_sshKey :: FilePath
181− , _deployInitOpts_hostname :: [String]
182− , _deployInitOpts_route :: String
183− , _deployInitOpts_adminEmail :: String
184− , _deployInitOpts_enableHttps :: Bool
185− }
186− deriving Show
187−
188188profileCommand :: Parser (String, [String])
189189profileCommand = (,)
190190 <$> strOption
@@ -203,15 +203,6 @@ profileCommand = (,)
203203 <> showDefault
204204 ))
205205
206−
207−--TODO: Result should provide normalised path and also original user input for error reporting.
208−thunkDirectoryParser :: Parser FilePath
209−thunkDirectoryParser = fmap (dropTrailingPathSeparator . normalise) . strArgument $ mconcat
210− [ action "directory"
211− , metavar "THUNKDIR"
212− , help "Path to directory containing thunk data"
213− ]
214−
215206thunkConfig :: Parser ThunkConfig
216207thunkConfig = ThunkConfig
217208 <$>
@@ -230,22 +221,36 @@ thunkPackConfig = ThunkPackConfig
230221 <$> 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.")
231222 <*> thunkConfig
232223
224+data ThunkOption = ThunkOption
225+ { _thunkOption_thunks :: NonEmpty FilePath
226+ , _thunkOption_command :: ThunkCommand
227+ } deriving Show
228+
233229data ThunkCommand
234− = ThunkCommand_Update [FilePath] ThunkUpdateConfig
235− | ThunkCommand_Unpack [FilePath]
236− | ThunkCommand_Pack [FilePath] ThunkPackConfig
230+ = ThunkCommand_Update ThunkUpdateConfig
231+ | ThunkCommand_Unpack
232+ | ThunkCommand_Pack ThunkPackConfig
237233 deriving Show
238234
239−thunkCommand :: Parser ThunkCommand
240−thunkCommand = hsubparser $ mconcat
241− [ command "update" $ info (ThunkCommand_Update <$> some thunkDirectoryParser <*> thunkUpdateConfig) $ progDesc "Update thunk to latest revision available"
242− , command "unpack" $ info (ThunkCommand_Unpack <$> some thunkDirectoryParser) $ progDesc "Unpack thunk into git checkout of revision it points to"
243− , command "pack" $ info (ThunkCommand_Pack <$> some thunkDirectoryParser <*> thunkPackConfig) $ progDesc "Pack git checkout into thunk that points at the current branch's upstream"
235+thunkOption :: Parser ThunkOption
236+thunkOption = hsubparser $ mconcat
237+ [ command "update" $ info (thunkOptionWith $ ThunkCommand_Update <$> thunkUpdateConfig) $ progDesc "Update packed thunk to latest revision available on the tracked branch"
238+ , command "unpack" $ info (thunkOptionWith $ pure ThunkCommand_Unpack) $ progDesc "Unpack thunk into git checkout of revision it points to"
239+ , command "pack" $ info (thunkOptionWith $ ThunkCommand_Pack <$> thunkPackConfig) $ progDesc "Pack git checkout or unpacked thunk into thunk that points at the current branch's upstream"
244240 ]
241+ where
242+ thunkOptionWith f = ThunkOption
243+ <$> ((NonEmpty.:|)
244+ <$> thunkDirArg (metavar "THUNKDIRS..." <> help "Paths to directories containing thunk data")
245+ <*> many (thunkDirArg mempty)
246+ )
247+ <*> f
248+ thunkDirArg opts = fmap (dropTrailingPathSeparator . normalise) $ strArgument $ action "directory" <> opts
245249
246250data ShellOpts
247251 = ShellOpts
248252 { _shellOpts_shell :: String
253+ , _shellOpts_interpretPaths :: [(FilePath, Interpret)]
249254 , _shellOpts_command :: Maybe String
250255 }
251256 deriving Show
@@ -256,9 +261,30 @@ shellFlags =
256261 <|> flag "ghc" "ghcjs" (long "ghcjs" <> help "Enter a shell having ghcjs rather than ghc")
257262 <|> strOption (short 'A' <> long "argument" <> metavar "NIXARG" <> help "Use the environment specified by the given nix argument of `shells'")
258263
264+interpretOpts :: Parser [(FilePath, Interpret)]
265+interpretOpts = many
266+ ( (, Interpret_Interpret) <$>
267+ strOption (common <> long "interpret" <> help
268+ "Don't pre-build packages found in DIR when constructing the package database. The default behavior is \
269+ \'--interpret <project-root>', which will load everything which is unpacked into GHCi. \
270+ \ Use --interpret and --no-interpret multiple times to add or remove multiple trees \
271+ \ from the environment. Settings for right-most directories will \
272+ \ override settings for any identical directories given earlier."
273+ )
274+ <|> (, Interpret_NoInterpret) <$>
275+ strOption (common <> long "no-interpret" <> help
276+ "Make packages found in DIR available in the package database (but only when they are used dependencies). \
277+ \ This will build the packages in DIR before loading GHCi. \
278+ \See help for --interpret for how the two options are related."
279+ )
280+ )
281+ where
282+ common = action "directory" <> metavar "DIR"
283+
259284shellOpts :: Parser ShellOpts
260285shellOpts = ShellOpts
261286 <$> shellFlags
287+ <*> interpretOpts
262288 <*> optional (strArgument (metavar "COMMAND"))
263289
264290portOpt :: Int -> Parser Int
@@ -269,9 +295,6 @@ parserPrefs = defaultPrefs
269295 { prefShowHelpOnEmpty = True
270296 }
271297
272−parseCLIArgs :: ArgsConfig -> [String] -> IO Args
273−parseCLIArgs cfg = handleParseResult . execParserPure parserPrefs (argsInfo cfg)
274−
275298-- | Create an Obelisk config for the current process.
276299mkObeliskConfig :: IO Obelisk
277300mkObeliskConfig = do
@@ -356,29 +379,7 @@ ob :: MonadObelisk m => ObCommand -> m ()
356379ob = \case
357380 ObCommand_Init source force -> initProject source force
358381 ObCommand_Deploy dc -> case dc of
359− DeployCommand_Init deployOpts -> withProjectRoot "." $ \root -> do
360− let deployDir = _deployInitOpts_outputDir deployOpts
361− r <- liftIO $ canonicalizePath root
362− rootEqualsTarget <- liftIO $ equalFilePath r <$> canonicalizePath deployDir
363− when rootEqualsTarget $
364− failWith $ "Deploy directory " <> T.pack deployDir <> " should not be the same as project root."
365− thunkPtr <- readThunk root >>= \case
366− Left err -> failWith $ case err of
367− ReadThunkError_AmbiguousFiles ->
368− "Project root " <> T.pack r <> " is not a git repository or valid thunk"
369− ReadThunkError_UnrecognizedFiles ->
370− "Project root " <> T.pack r <> " is not a git repository or valid thunk"
371− _ -> "thunk read: " <> T.pack (show err)
372− Right (ThunkData_Packed ptr) -> return ptr
373− Right (ThunkData_Checkout (Just ptr)) -> return ptr
374− Right (ThunkData_Checkout Nothing) ->
375− getThunkPtr False root Nothing
376− let sshKeyPath = _deployInitOpts_sshKey deployOpts
377− hostname = _deployInitOpts_hostname deployOpts
378− route = _deployInitOpts_route deployOpts
379− adminEmail = _deployInitOpts_adminEmail deployOpts
380− enableHttps = _deployInitOpts_enableHttps deployOpts
381− deployInit thunkPtr deployDir sshKeyPath hostname route adminEmail enableHttps
382+ DeployCommand_Init deployOpts -> withProjectRoot "." $ \root -> deployInit deployOpts root
382383 DeployCommand_Push remoteBuilder -> do
383384 deployPath <- liftIO $ canonicalizePath "."
384385 deployPush deployPath $ case remoteBuilder of
@@ -386,23 +387,36 @@ ob = \case
386387 Just RemoteBuilder_ObeliskVM -> (:[]) <$> VmBuilder.getNixBuildersArg
387388 DeployCommand_Update -> deployUpdate "."
388389 DeployCommand_Test (platform, extraArgs) -> deployMobile platform extraArgs
389− ObCommand_Run -> run
390+ ObCommand_Run interpretPathsList -> withInterpretPaths interpretPathsList run
390391 ObCommand_Profile basePath rtsFlags -> profile basePath rtsFlags
391− ObCommand_Thunk tc -> case tc of
392− ThunkCommand_Update thunks config -> for_ thunks (updateThunkToLatest config)
393− ThunkCommand_Unpack thunks -> for_ thunks unpackThunk
394− ThunkCommand_Pack thunks config -> for_ thunks (packThunk config)
395− ObCommand_Repl -> runRepl
396− ObCommand_Watch -> runWatch
397− ObCommand_Shell so -> withProjectRoot "." $ \root ->
398− projectShell root False (_shellOpts_shell so) (_shellOpts_command so)
399− ObCommand_Doc shell' pkgs -> withProjectRoot "." $ \root ->
400− projectShell root False shell' (Just $ haddockCommand pkgs)
392+ ObCommand_Thunk to -> case _thunkOption_command to of
393+ ThunkCommand_Update config -> for_ thunks (updateThunkToLatest config)
394+ ThunkCommand_Unpack -> for_ thunks unpackThunk
395+ ThunkCommand_Pack config -> for_ thunks (packThunk config)
396+ where
397+ thunks = _thunkOption_thunks to
398+ ObCommand_Repl interpretPathsList -> withInterpretPaths interpretPathsList runRepl
399+ ObCommand_Watch interpretPathsList -> withInterpretPaths interpretPathsList runWatch
400+ ObCommand_Shell (ShellOpts shellAttr interpretPathsList cmd) -> withInterpretPaths interpretPathsList $ \root interpretPaths ->
401+ nixShellForInterpretPaths False shellAttr root interpretPaths cmd
402+ ObCommand_Doc shellAttr pkgs -> withProjectRoot "." $ \root ->
403+ nixShellForInterpretPaths True shellAttr root emptyPathTree $ Just $ haddockCommand pkgs
401404 ObCommand_Hoogle shell' port -> withProjectRoot "." $ \root -> do
402405 nixShellWithHoogle root True shell' $ Just $ "hoogle server -p " <> show port <> " --local"
403406 ObCommand_Internal icmd -> case icmd of
404407 ObInternal_ApplyPackages origPath inPath outPath packagePaths -> do
405408 liftIO $ Preprocessor.applyPackages origPath inPath outPath packagePaths
409+ ObInternal_ExportGhciConfig interpretPathsList useRelativePaths ->
410+ liftIO . putStrLn . unlines =<< withInterpretPaths interpretPathsList (exportGhciConfig useRelativePaths)
411+
412+-- | A helper for the common case that the command you want to run needs the project root and a resolved
413+-- set of interpret paths.
414+withInterpretPaths :: MonadObelisk m => [(FilePath, Interpret)] -> (FilePath -> PathTree Interpret -> m a) -> m a
415+withInterpretPaths interpretPathsList f = withProjectRoot "." $ \root -> do
416+ interpretPaths' <- resolveInterpretPaths $ (root, Interpret_Interpret) : interpretPathsList
417+ case interpretPaths' of
418+ Nothing -> failWith "No paths provided for finding packages"
419+ Just interpretPaths -> f root interpretPaths
406420
407421haddockCommand :: [String] -> String
408422haddockCommand pkgs = unwords
@@ -417,14 +431,21 @@ haddockCommand pkgs = unwords
417431getArgsConfig :: IO ArgsConfig
418432getArgsConfig = pure $ ArgsConfig { _argsConfig_enableVmBuilderByDefault = System.Info.os == "darwin" }
419433
420−encodeStaticKey :: StaticKey -> String
421−encodeStaticKey = T.unpack . decodeUtf8With lenientDecode . Base16.encode . LBS.toStrict . Binary.encode
422−
423−-- TODO: Use failWith in place of fail to be consistent.
424−decodeStaticKey :: String -> Either String StaticKey
425−decodeStaticKey s = case Base16.decode $ encodeUtf8 $ T.pack s of
426− (b, "") -> case Binary.decodeOrFail $ LBS.fromStrict b of
427− Right ("", _, a) -> pure a
428− Right _ -> fail "decodeStaticKey: Binary.decodeOrFail didn't consume all input"
429− Left (_, _, e) -> fail $ "decodeStaticKey: Binary.decodeOrFail failed: " <> show e
430− _ -> fail $ "decodeStaticKey: could not decode hex string: " <> show s
434+-- | Resolves an ordered list of paths for use with @--interpret@/@--no-interpret@ by coalescing
435+-- paths into a non-ambiguous set of paths. Ambiguity is resolved by choosing right-most paths
436+-- over any preceeding identical paths.
437+--
438+-- For example: @a/b=ON a/b/c=OFF@ and @a/b/c=OFF a/b=ON@ are the same.
439+-- @a/b=ON a/b=OFF@ is reduced to @a/b=OFF@. We prefer right-biased choice to increase
440+-- scriptability.
441+--
442+-- N.B. All the paths in the result will be canonicalized. It's impossible to determine path
443+-- overlap otherwise.
444+resolveInterpretPaths :: MonadIO m => [(FilePath, a)] -> m (Maybe (PathTree a))
445+resolveInterpretPaths ps = do
446+ trees <- liftIO $ for ps $ \(p, a) -> pathToTree a <$> canonicalizePath p
447+ pure $ foldr1 mergeTrees <$> nonEmpty trees
448+ where
449+ -- | Merge two 'PathTree's preferring leaves on the right in as much as they overlap with paths on the left.
450+ mergeTrees :: PathTree a -> PathTree a -> PathTree a
451+ mergeTrees (PathTree_Node ax x) (PathTree_Node ay y) = PathTree_Node (ay <|> ax) $ Map.unionWith mergeTrees x y
lib/command/src/Obelisk/Command/Deploy.hs+33−16
@@ -1,21 +1,24 @@
1+{-# LANGUAGE DeriveGeneric #-}
12{-# LANGUAGE FlexibleContexts #-}
23{-# LANGUAGE LambdaCase #-}
34{-# LANGUAGE OverloadedStrings #-}
5+{-# LANGUAGE QuasiQuotes #-}
46{-# LANGUAGE ScopedTypeVariables #-}
57{-# LANGUAGE ViewPatterns #-}
6−{-# LANGUAGE DeriveGeneric #-}
78module Obelisk.Command.Deploy where
89
10+import Control.Applicative (liftA2)
911import Control.Lens
1012import Control.Monad
1113import Control.Monad.Catch (Exception (displayException), MonadThrow, bracket, throwM, try)
12−import Control.Monad.IO.Class (liftIO)
14+import Control.Monad.IO.Class (MonadIO, liftIO)
1315import Data.Aeson (FromJSON, ToJSON, encode, eitherDecode)
1416import Data.Bits
1517import qualified Data.ByteString.Lazy as BSL
1618import Data.Default
1719import qualified Data.Map as Map
1820import qualified Data.Set as Set
21+import Data.String.Here.Interpolated (i)
1922import qualified Data.Text as T
2023import qualified Data.Text.IO as T
2124import GHC.Generics
@@ -36,18 +39,32 @@ import Obelisk.Command.Project
3639import Obelisk.Command.Thunk
3740import Obelisk.Command.Utils
3841
42+data DeployInitOpts = DeployInitOpts
43+ { _deployInitOpts_outputDir :: FilePath
44+ , _deployInitOpts_sshKey :: FilePath
45+ , _deployInitOpts_hostname :: [String]
46+ , _deployInitOpts_route :: String
47+ , _deployInitOpts_adminEmail :: String
48+ , _deployInitOpts_enableHttps :: Bool
49+ } deriving Show
50+
51+deployInit :: MonadObelisk m => DeployInitOpts -> FilePath -> m ()
52+deployInit deployOpts root = do
53+ let deployDir = _deployInitOpts_outputDir deployOpts
54+ rootEqualsTarget <- liftIO $ liftA2 equalFilePath (canonicalizePath root) (canonicalizePath deployDir)
55+ when rootEqualsTarget $
56+ failWith [i|Deploy directory ${deployDir} should not be the same as project root.|]
57+ thunkPtr <- readThunk root >>= \case
58+ Right (ThunkData_Packed _ ptr) -> return ptr
59+ _ -> getThunkPtr CheckClean_NotIgnored root Nothing
60+ deployInit' thunkPtr deployOpts
3961
40−deployInit
62+deployInit'
4163 :: MonadObelisk m
4264 => ThunkPtr
43− -> FilePath
44− -> FilePath
45− -> [String] -- ^ hostnames
46− -> String -- ^ route
47− -> String -- ^ admin email
48− -> Bool -- ^ enable https
65+ -> DeployInitOpts
4966 -> m ()
50−deployInit thunkPtr deployDir sshKeyPath hostnames route adminEmail enableHttps = do
67+deployInit' thunkPtr (DeployInitOpts deployDir sshKeyPath hostnames route adminEmail enableHttps) = do
5168 liftIO $ createDirectoryIfMissing True deployDir
5269 localKey <- withSpinner ("Preparing " <> T.pack deployDir) $ do
5370 localKey <- liftIO (doesFileExist sshKeyPath) >>= \case
@@ -77,8 +94,8 @@ deployInit thunkPtr deployDir sshKeyPath hostnames route adminEmail enableHttps
7794 ]
7895
7996 let srcDir = deployDir </> "src"
80− withSpinner ("Creating source thunk (" <> T.pack (makeRelative deployDir srcDir) <> ")") $ liftIO $ do
81− createThunk srcDir thunkPtr
97+ withSpinner ("Creating source thunk (" <> T.pack (makeRelative deployDir srcDir) <> ")") $ do
98+ createThunk srcDir $ Right thunkPtr
8299 setupObeliskImpl deployDir
83100
84101 withSpinner "Writing deployment configuration" $ do
@@ -92,8 +109,8 @@ deployInit thunkPtr deployDir sshKeyPath hostnames route adminEmail enableHttps
92109 withSpinner ("Initializing git repository (" <> T.pack deployDir <> ")") $
93110 initGit deployDir
94111
95−setupObeliskImpl :: FilePath -> IO ()
96−setupObeliskImpl deployDir = do
112+setupObeliskImpl :: MonadIO m => FilePath -> m ()
113+setupObeliskImpl deployDir = liftIO $ do
97114 let
98115 implDir = toImplDir deployDir
99116 goBackUp = foldr (</>) "" $ (".." <$) $ splitPath $ makeRelative deployDir implDir
@@ -109,8 +126,8 @@ deployPush deployPath getNixBuilders = do
109126 routeHost <- getHostFromRoute enableHttps route
110127 let srcPath = deployPath </> "src"
111128 thunkPtr <- readThunk srcPath >>= \case
112− Right (ThunkData_Packed ptr) -> return ptr
113− Right (ThunkData_Checkout _) -> do
129+ Right (ThunkData_Packed _ ptr) -> return ptr
130+ Right ThunkData_Checkout -> do
114131 checkGitCleanStatus srcPath True >>= \case
115132 True -> packThunk (ThunkPackConfig False (ThunkConfig Nothing)) srcPath
116133 False -> failWith $ T.pack $ "ob deploy push: ensure " <> srcPath <> " has no pending changes and latest is pushed upstream."
lib/command/src/Obelisk/Command/Nix.hs+4−4
@@ -207,7 +207,7 @@ nixCmdProc' cmdCfg = (proc (T.unpack cmd) options, cmd)
207207 )
208208
209209nixCmd :: MonadObelisk m => NixCmd -> m FilePath
210−nixCmd cmdCfg = withSpinner' ("Running " <> cmd <> desc) (Just $ const $ "Built " <> desc) $ do
210+nixCmd cmdCfg = withSpinner' (T.unwords $ "Running" : cmd : desc) (Just $ const $ T.unwords $ "Built" : desc) $ do
211211 output <- readProcessAndLogStderr Debug cmdProc
212212 -- Remove final newline that Nix appends
213213 Just (outPath, '\n') <- pure $ T.unsnoc output
@@ -218,7 +218,7 @@ nixCmd cmdCfg = withSpinner' ("Running " <> cmd <> desc) (Just $ const $ "Built
218218 NixCmd_Build cfg' -> cfg' ^. nixCommonConfig
219219 NixCmd_Instantiate cfg' -> cfg' ^. nixCommonConfig
220220 path = commonCfg ^. nixCmdConfig_target . target_path
221− desc = T.pack $ mconcat $ catMaybes
222− [ ("on " <>) <$> path
223− , (\a -> " [" <> a <> "]") <$> (commonCfg ^. nixCmdConfig_target . target_attr)
221+ desc = concat $ catMaybes
222+ [ (\x -> ["on", T.pack x]) <$> path
223+ , (\a -> ["[" <> T.pack a <> "]"]) <$> (commonCfg ^. nixCmdConfig_target . target_attr)
224224 ]
lib/command/src/Obelisk/Command/Preprocessor.hs+18−19
@@ -11,49 +11,48 @@ import qualified Data.Text.Lazy.Builder as TL
1111import qualified Data.Text.Lazy.Encoding as TL
1212import Distribution.Compiler (CompilerFlavor (..))
1313import Language.Haskell.Extension (Extension (..), Language(..))
14−import System.IO (IOMode (..), hClose, hPutStrLn, openFile, stderr)
15−import System.FilePath (hasTrailingPathSeparator, joinPath, normalise, splitPath)
14+import System.Directory (canonicalizePath)
15+import System.IO (IOMode (..), hPutStrLn, stderr, withFile)
16+import System.FilePath (hasTrailingPathSeparator, joinPath, splitPath)
1617
1718import Obelisk.Command.Run (CabalPackageInfo (..), parseCabalPackage')
1819
1920applyPackages :: FilePath -> FilePath -> FilePath -> [FilePath] -> IO ()
20−applyPackages origPath inPath outPath packagePaths = do
21+applyPackages origPath inPath outPath packagePaths' = withFile outPath WriteMode $ \outFile -> do
2122 -- This code is intended to be executed via ghci's -pgmF preprocessor option
2223 -- The command line arguments are passed in via ghc, which dictates the first three options and meanings
23− -- In order for this code to execute, origPath must contain either a '.' character or a '/' character.
24+ -- In order for this code to execute, origPath must contain either a '.' character or a '/' character.
2425 -- (This is to avoid the possibility of the command line syntax conflicting with another ob command)
2526 -- We do have control over the remaining arguments, but they must be the same for all files.
2627 -- Thus, the fourth command line argument must be "apply-packages", which has already been handled.
2728 -- We assume all the remaining arguments passed in are paths to cabal or hpack package specifications.
28−
29− outFile <- openFile outPath WriteMode
30− let hPutTextBuilder h = BU.hPutBuilder h . TL.encodeUtf8Builder . TL.toLazyText
31−
32− -- putStr "--------------------------------------------------------------------------------\n"
33− -- print args
34− -- putStr "--------------------------------------------------------------------------------\n"
35−
3629 -- Thus we must select among the packagePaths for the file we are going to parse.
3730
31+ origPathCanonical <- canonicalizePath origPath
32+ packagePaths <- traverse canonicalizePath packagePaths'
33+
3834 let takeDirs = takeWhile hasTrailingPathSeparator
39− packageDirs = sortOn (negate . length . takeDirs) $ map (splitPath . normalise) packagePaths
40− origDir = splitPath $ normalise origPath
35+ packageDirs = sortOn (negate . length . takeDirs) $ map splitPath packagePaths
36+ origDir = splitPath origPathCanonical
4137 matches = [ joinPath d | d <- packageDirs, takeDirs d `isPrefixOf` origDir ]
4238
4339 -- So the first element of matches is going to be the deepest path to a package spec that contains
4440 -- our file as a subdirectory.
4541
4642 case matches of
47− [] -> hPutTextBuilder outFile (lineNumberPragma origPath) -- TODO: probably should produce a warning
48− (packagePath:_) -> do
43+ [] ->
44+ hPutStrLn stderr $ "Error: Unable to find cabal information for " <> origPath <> "; Skipping preprocessor."
45+ packagePath:_ -> do
4946 parseCabalPackage' packagePath >>= \case
5047 Left err ->
51− hPutStrLn stderr $ "Error: Unable to parse cabal package " <> packagePath <> "; Skipping preprocessor on " <> origPath <> ". Error was " <> show err
48+ hPutStrLn stderr $ "Error: Unable to parse cabal package " <> packagePath <> "; Skipping preprocessor on " <> origPath <> ". Error: " <> show err
5249 Right (_warnings, packageInfo) ->
5350 hPutTextBuilder outFile (generateHeader origPath packageInfo)
5451
5552 BL.readFile inPath >>= BL.hPut outFile
56− hClose outFile
53+
54+ where
55+ hPutTextBuilder h = BU.hPutBuilder h . TL.encodeUtf8Builder . TL.toLazyText
5756
5857
5958 -- I'm pretty sure there's a certain amount of oversimplification in CabalPackageInfo, so I doubt this is fully robust.
@@ -87,7 +86,7 @@ generateHeader origPath packageInfo =
8786 <> TL.fromText " #-}\n"
8887 else mempty
8988 optList = map TL.fromString
90− $ filter (\x -> not (isPrefixOf "-O" x))
89+ $ filter (not . isPrefixOf "-O")
9190 $ fromMaybe []
9291 $ lookup GHC (_cabalPackageInfo_compilerOptions packageInfo)
9392
lib/command/src/Obelisk/Command/Project.hs+43−28
@@ -10,11 +10,9 @@ module Obelisk.Command.Project
1010 , nixShellRunConfig
1111 , nixShellRunProc
1212 , nixShellWithHoogle
13− , nixShellWithPkgs
13+ , nixShellWithoutPkgs
1414 , obeliskDirName
15− , projectShell
1615 , toImplDir
17− , toNixPath
1816 , toObeliskDir
1917 , withProjectRoot
2018 ) where
@@ -29,7 +27,6 @@ import Data.Bits
2927import qualified Data.ByteString.Lazy as BSL
3028import Data.Default (def)
3129import Data.Function ((&), on)
32−import Data.List (isInfixOf)
3330import Data.Map (Map)
3431import Data.Text (Text)
3532import qualified Data.Text as T
@@ -50,7 +47,7 @@ import Obelisk.App (MonadObelisk)
5047import Obelisk.CliApp
5148import Obelisk.Command.Nix
5249import Obelisk.Command.Thunk
53−import Obelisk.Command.Utils (nixExePath, nixBuildExePath)
50+import Obelisk.Command.Utils (nixBuildExePath, nixExePath, toNixPath)
5451
5552--TODO: Make this module resilient to random exceptions
5653
@@ -96,16 +93,22 @@ initProject source force = withSystemTempDirectory "ob-init" $ \tmpDir -> do
9693 | otherwise -> failWith "ob init requires an empty directory. Use the flag --force to init anyway, potentially overwriting files."
9794 skeleton <- withSpinner "Setting up obelisk" $ do
9895 liftIO $ createDirectory obDir
96+ -- Clone the git source and repack it with the init source obelisk
97+ -- The purpose of this is to ensure we use the correct thunk spec.
98+ let cloneAndRepack src = do
99+ putLog Debug $ "Cloning obelisk into " <> T.pack implDir <> " and repacking using itself"
100+ commit <- getLatestRev src
101+ gitCloneForThunkUnpack (thunkSourceToGitSource src) (_thunkRev_commit commit) implDir
102+ callHandoffOb implDir ["thunk", "pack", implDir]
99103 case source of
100− InitSource_Default -> createThunkWithLatest implDir obeliskSource
101− InitSource_Branch branch -> createThunkWithLatest implDir $ obeliskSourceWithBranch branch
104+ InitSource_Default -> cloneAndRepack obeliskSource
105+ InitSource_Branch branch -> cloneAndRepack $ obeliskSourceWithBranch branch
102106 InitSource_Symlink path -> do
103107 let symlinkPath = if isAbsolute path
104108 then path
105109 else ".." </> path
106110 liftIO $ createSymbolicLink symlinkPath implDir
107111 _ <- nixBuildAttrWithCache implDir "command"
108− --TODO: We should probably handoff to the impl here
109112 skel <- nixBuildAttrWithCache implDir "skeleton" --TODO: I don't think there's actually any reason to cache this
110113
111114 callProcessAndLogOutput (Notice, Error) $
@@ -131,6 +134,24 @@ initProject source force = withSystemTempDirectory "ob-init" $ \tmpDir -> do
131134 createDirectoryIfMissing False configDir
132135 mapM_ (createDirectoryIfMissing False . (configDir </>)) ["backend", "common", "frontend"]
133136
137+callHandoffOb
138+ :: MonadObelisk m
139+ => FilePath -- ^ Directory of the obelisk we want to handoff to
140+ -> [String] -- ^ Arguments to pass to ob
141+ -> m ()
142+callHandoffOb dir args = do
143+ obeliskCommandPkg <- nixCmd $ NixCmd_Build $ def
144+ & nixBuildConfig_outLink .~ OutLink_None
145+ & nixCmdConfig_target .~ Target
146+ { _target_path = Just dir
147+ , _target_attr = Just "command"
148+ , _target_expr = Nothing
149+ }
150+ let impl = obeliskCommandPkg </> "bin" </> "ob"
151+ -- Invoke the real implementation, using --no-handoff to prevent infinite recursion
152+ putLog Debug $ "Running '" <> T.pack (unwords args) <> "' with " <> T.pack impl
153+ callProcessAndLogOutput (Debug, Warning) (proc impl ("--no-handoff" : args))
154+
134155--TODO: Allow the user to ignore our security concerns
135156-- | Find the Obelisk implementation for the project at the given path
136157findProjectObeliskCommand :: MonadObelisk m => FilePath -> m (Maybe FilePath)
@@ -249,13 +270,6 @@ filePermissionIsSafe s umask = not fileWorldWritable && fileGroupWritable <= uma
249270 fileGroupWritable = fileMode s .&. 0o020 == 0o020
250271 umaskGroupWritable = umask .&. 0o020 == 0
251272
252−-- | Nix syntax requires relative paths to be prefixed by @./@ or
253−-- @../@. This will make a 'FilePath' that can be embedded in a Nix
254−-- expression.
255−toNixPath :: FilePath -> FilePath
256−toNixPath root | "/" `isInfixOf` root = root
257− | otherwise = "./" <> root
258−
259273nixShellRunConfig :: MonadObelisk m => FilePath -> Bool -> Maybe String -> m NixShellConfig
260274nixShellRunConfig root isPure command = do
261275 nixpkgsPath <- fmap T.strip $ readProcessAndLogStderr Debug $ setCwd (Just root) $
@@ -273,19 +287,28 @@ nixShellRunConfig root isPure command = do
273287nixShellRunProc :: NixShellConfig -> ProcessSpec
274288nixShellRunProc cfg = setDelegateCtlc True $ proc "nix-shell" $ runNixShellConfig cfg
275289
276−nixShellWithPkgs :: MonadObelisk m => FilePath -> Bool -> Bool -> Map Text FilePath -> Maybe String -> m ()
277−nixShellWithPkgs root isPure chdirToRoot packageNamesAndPaths command = do
290+nixShellWithoutPkgs
291+ :: MonadObelisk m
292+ => FilePath -- ^ Path to project root
293+ -> Bool -- ^ Should this be a pure shell?
294+ -> Bool -- ^ Should we chdir to the package root in the shell?
295+ -> Map Text FilePath -- ^ Package names mapped to their paths
296+ -> String -- ^ Shell attribute to use (e.g. @"ghc"@, @"ghcjs"@, etc.)
297+ -> Maybe String -- ^ If 'Just' run the given command; otherwise just open the interactive shell
298+ -> m ()
299+nixShellWithoutPkgs root isPure chdirToRoot packageNamesAndPaths shellAttr command = do
278300 packageNamesAndAbsPaths <- liftIO $ for packageNamesAndPaths makeAbsolute
279301 defShellConfig <- nixShellRunConfig root isPure command
280302 let setCwd_ = if chdirToRoot then setCwd (Just root) else id
281− (_, _, _, ph) <- createProcess_ "nixShellWithPkgs" $ setCwd_ $ nixShellRunProc $ defShellConfig
303+ (_, _, _, ph) <- createProcess_ "nixShellWithoutPkgs" $ setCwd_ $ nixShellRunProc $ defShellConfig
282304 & nixShellConfig_common . nixCmdConfig_target . target_expr ?~
283− "{root, pkgs}: ((import root {}).passthru.__unstable__.self.extend (_: _: {\
305+ "{root, pkgs, shell}: ((import root {}).passthru.__unstable__.self.extend (_: _: {\
284306 \shellPackages = builtins.fromJSON pkgs;\
285− \})).project.shells.ghc"
307+ \})).project.shells.${shell}"
286308 & nixShellConfig_common . nixCmdConfig_args .~
287309 [ rawArg "root" $ toNixPath $ if chdirToRoot then "." else root
288310 , strArg "pkgs" (T.unpack $ decodeUtf8 $ BSL.toStrict $ Json.encode packageNamesAndAbsPaths)
311+ , strArg "shell" shellAttr
289312 ]
290313 void $ waitForProcess ph
291314
@@ -300,14 +323,6 @@ nixShellWithHoogle root isPure shell' command = do
300323 & nixShellConfig_common . nixCmdConfig_args .~ [ strArg "shell" shell' ]
301324 void $ waitForProcess ph
302325
303−projectShell :: MonadObelisk m => FilePath -> Bool -> String -> Maybe String -> m ()
304−projectShell root isPure shellName command = do
305− defShellConfig <- nixShellRunConfig root isPure command
306− (_, _, _, ph) <- createProcess_ "runNixShellAttr" $ setCwd (Just root) $ nixShellRunProc $ defShellConfig
307− & nixShellConfig_common . nixCmdConfig_target . target_path ?~ "default.nix"
308− & nixShellConfig_common . nixCmdConfig_target . target_attr ?~ ("shells." <> shellName)
309− void $ waitForProcess ph
310−
311326findProjectAssets :: MonadObelisk m => FilePath -> m Text
312327findProjectAssets root = do
313328 isDerivation <- readProcessAndLogStderr Debug $ setCwd (Just root) $
lib/command/src/Obelisk/Command/Run.hs+226−74
@@ -1,10 +1,14 @@
1+{-# LANGUAGE DeriveFoldable #-}
2+{-# LANGUAGE DeriveFunctor #-}
3+{-# LANGUAGE DeriveTraversable #-}
14{-# LANGUAGE FlexibleContexts #-}
25{-# LANGUAGE LambdaCase #-}
36{-# LANGUAGE MultiParamTypeClasses #-}
47{-# LANGUAGE OverloadedStrings #-}
8+{-# LANGUAGE QuasiQuotes #-}
59{-# LANGUAGE ScopedTypeVariables #-}
610{-# LANGUAGE TupleSections #-}
7−{-# LANGUAGE QuasiQuotes #-}
11+{-# LANGUAGE ViewPatterns #-}
812module Obelisk.Command.Run where
913
1014import Control.Arrow ((&&&))
@@ -14,15 +18,22 @@ import Control.Monad (filterM, unless, void)
1418import Control.Monad.Except (runExceptT, throwError)
1519import Control.Monad.IO.Class (liftIO)
1620import Control.Monad.Reader (MonadIO)
21+import Data.Bifoldable (bifoldr1)
22+import Data.Bifunctor (bimap)
1723import Data.Coerce (coerce)
1824import Data.Default (def)
19−import Data.Either
20−import Data.Foldable (for_, toList)
25+import Data.Either (partitionEithers)
26+import Data.Foldable (fold, for_, toList)
27+import Data.Functor.Identity (runIdentity)
28+import Data.List.NonEmpty (NonEmpty)
2129import qualified Data.List.NonEmpty as NE
2230import Data.Map (Map)
2331import qualified Data.Map as Map
32+import qualified Data.Map.Monoidal as MMap
2433import Data.Maybe
34+import Data.Set (Set)
2535import qualified Data.Set as Set
36+import Data.String.Here.Interpolated (i)
2637import Data.Text (Text)
2738import qualified Data.Text as T
2839import Data.Time.Clock (getCurrentTime)
@@ -65,7 +76,7 @@ import Obelisk.CliApp (
6576 withSpinner,
6677 )
6778import Obelisk.Command.Nix
68−import Obelisk.Command.Project (nixShellWithPkgs, toImplDir, withProjectRoot, findProjectAssets)
79+import Obelisk.Command.Project (nixShellWithoutPkgs, withProjectRoot, findProjectAssets)
6980import Obelisk.Command.Thunk (attrCacheFileName)
7081import Obelisk.Command.Utils (findExePath, ghcidExePath)
7182
@@ -84,6 +95,34 @@ data CabalPackageInfo = CabalPackageInfo
8495 -- ^ List of compiler-specific options (e.g., the "ghc-options" field of the cabal file)
8596 }
8697
98+-- | 'Bool' with a better name for it's purpose.
99+data Interpret = Interpret_Interpret | Interpret_NoInterpret deriving (Eq, Ord, Show)
100+
101+textInterpret :: Interpret -> Text
102+textInterpret = \case
103+ Interpret_Interpret -> "Interpret"
104+ Interpret_NoInterpret -> "NoInterpret"
105+
106+-- | Describe a set of 'FilePath's as a tree to facilitate merging them in a convenient way.
107+data PathTree a = PathTree_Node
108+ (Maybe a) -- An optional leaf at this point in the tree
109+ (Map FilePath (PathTree a)) -- Branches to deeper leaves
110+ deriving (Eq, Ord, Show, Functor, Foldable, Traversable)
111+
112+emptyPathTree :: PathTree a
113+emptyPathTree = PathTree_Node Nothing mempty
114+
115+-- | 2D ASCII drawing of a 'PathTree'. Adapted from Data.Tree.draw.
116+drawPathTree :: (a -> Text) -> PathTree a -> Text
117+drawPathTree showA (PathTree_Node _ ts0) = T.intercalate "\n" $ goForest (Map.toList ts0)
118+ where
119+ annotated ma = maybe id (\a b -> b <> " [" <> showA a <> "]") ma . T.pack
120+ goTree (fp, PathTree_Node ma forest) = annotated ma fp : goForest (Map.toList forest)
121+ goForest [] = []
122+ goForest [tree] = shift "└─ " " " (goTree tree)
123+ goForest (tree:forest) = shift "├─ " "│ " (goTree tree) <> goForest forest
124+ shift first other = zipWith (<>) (first : repeat other)
125+
87126-- | Used to signal to obelisk that it's being invoked as a preprocessor
88127preprocessorIdentifier :: String
89128preprocessorIdentifier = "__preprocessor-apply-packages"
@@ -122,16 +161,15 @@ profile profileBasePattern rtsFlags = withProjectRoot "." $ \root -> do
122161 <> [ "-RTS" ]
123162 void $ waitForProcess ph
124163
125−run
126− :: MonadObelisk m
127− => m ()
128−run = withProjectRoot "." $ \root -> do
129− pkgs <- fmap toList . parsePackagesOrFail =<< getLocalPkgs root
130− withGhciScript pkgs root $ \dotGhciPath -> do
131− freePort <- getFreePort
164+run :: MonadObelisk m => FilePath -> PathTree Interpret -> m ()
165+run root interpretPaths = do
166+ pkgs <- getParsedLocalPkgs root interpretPaths
167+ withGhciScript pkgs $ \dotGhciPath -> do
132168 assets <- findProjectAssets root
133169 putLog Debug $ "Assets impurely loaded from: " <> assets
134− runGhcid root True dotGhciPath pkgs $ Just $ unwords
170+ ghciArgs <- getGhciSessionSettings pkgs root True
171+ freePort <- getFreePort
172+ runGhcid root True (ghciArgs <> mkGhciScriptArg dotGhciPath) pkgs $ Just $ unwords
135173 [ "Obelisk.Run.run"
136174 , show freePort
137175 , "(Obelisk.Run.runServeAsset " ++ show assets ++ ")"
@@ -139,39 +177,87 @@ run = withProjectRoot "." $ \root -> do
139177 , "Frontend.frontend"
140178 ]
141179
142−runRepl :: MonadObelisk m => m ()
143−runRepl = withProjectRoot "." $ \root -> do
144− pkgs <- fmap toList . parsePackagesOrFail =<< getLocalPkgs root
145− withGhciScript pkgs "." $ \dotGhciPath ->
146− runGhciRepl root pkgs dotGhciPath
180+runRepl :: MonadObelisk m => FilePath -> PathTree Interpret -> m ()
181+runRepl root interpretPaths = do
182+ pkgs <- getParsedLocalPkgs root interpretPaths
183+ ghciArgs <- getGhciSessionSettings pkgs "." True
184+ withGhciScript pkgs $ \dotGhciPath ->
185+ runGhciRepl root pkgs (ghciArgs <> mkGhciScriptArg dotGhciPath)
147186
148−runWatch :: MonadObelisk m => m ()
149−runWatch = withProjectRoot "." $ \root -> do
150− pkgs <- fmap toList . parsePackagesOrFail =<< getLocalPkgs root
151− withGhciScript pkgs root $ \dotGhciPath ->
152− runGhcid root True dotGhciPath pkgs Nothing
187+runWatch :: MonadObelisk m => FilePath -> PathTree Interpret -> m ()
188+runWatch root interpretPaths = do
189+ pkgs <- getParsedLocalPkgs root interpretPaths
190+ ghciArgs <- getGhciSessionSettings pkgs root True
191+ withGhciScript pkgs $ \dotGhciPath ->
192+ runGhcid root True (ghciArgs <> mkGhciScriptArg dotGhciPath) pkgs Nothing
193+
194+exportGhciConfig :: MonadObelisk m => Bool -> FilePath -> PathTree Interpret -> m [String]
195+exportGhciConfig useRelativePaths root interpretPaths = do
196+ pkgs <- getParsedLocalPkgs root interpretPaths
197+ getGhciSessionSettings pkgs "." useRelativePaths
198+
199+nixShellForInterpretPaths :: MonadObelisk m => Bool -> String -> FilePath -> PathTree Interpret -> Maybe String -> m ()
200+nixShellForInterpretPaths isPure shell root interpretPaths cmd = do
201+ pkgs <- getParsedLocalPkgs root interpretPaths
202+ nixShellWithoutPkgs root isPure False (packageInfoToNamePathMap pkgs) shell cmd
203+
204+-- | Like 'getLocalPkgs' but also parses them and fails if any of them can't be parsed.
205+getParsedLocalPkgs :: MonadObelisk m => FilePath -> PathTree Interpret -> m (NonEmpty CabalPackageInfo)
206+getParsedLocalPkgs root interpretPaths = parsePackagesOrFail =<< getLocalPkgs root interpretPaths
153207
154208-- | Relative paths to local packages of an obelisk project.
155209--
156210-- These are a combination of the obelisk predefined local packages,
157211-- and any packages that the user has set with the @packages@ argument
158212-- to the Nix @project@ function.
159−getLocalPkgs :: MonadObelisk m => FilePath -> m [FilePath]
160−getLocalPkgs root = do
161− obeliskPaths <- runFind ["-L", root, "-name", ".obelisk", "-type", "d"]
213+getLocalPkgs :: forall m. MonadObelisk m => FilePath -> PathTree Interpret -> m (Set FilePath)
214+getLocalPkgs root interpretPaths = do
215+ putLog Debug $ [i|Finding packages with root ${root} and interpret paths:|] <> "\n" <> drawPathTree textInterpret interpretPaths
216+ obeliskPackagePaths <- runFind ["-L", root, "-name", ".obelisk", "-type", "d"]
162217
163218 -- We do not want to find packages that are embedded inside other obelisk projects, unless that
164219 -- obelisk project is our own.
165− let exclusions = filter (/= root) $ map takeDirectory obeliskPaths
166− fmap (map (makeRelative ".")) $ runFind $
167− ["-L", root, "(", "-name", "*.cabal", "-o", "-name", Hpack.packageConfig, ")", "-a", "-type", "f"]
168− <> concat [["-not", "-path", p </> "*"] | p <- (toImplDir "*" </> attrCacheFileName) : exclusions]
220+ obeliskPackageExclusions <- liftIO $ fmap Set.fromList $ traverse canonicalizePath $
221+ filter (/= root) $ map takeDirectory obeliskPackagePaths
222+ putLog Debug [i|Excluding obelisk packages: ${T.pack $ unwords $ Set.toList obeliskPackageExclusions}|]
223+ let rootsAndExclusions = calcIntepretFinds "" interpretPaths
224+
225+ fmap fold $ for (MMap.toAscList rootsAndExclusions) $ \(interpretPathRoot, exclusions) ->
226+ let allExclusions = obeliskPackageExclusions <> exclusions <> Set.singleton ("*" </> attrCacheFileName)
227+ in fmap (Set.fromList . map normalise) $ runFind $
228+ ["-L", interpretPathRoot, "(", "-name", "*.cabal", "-o", "-name", Hpack.packageConfig, ")", "-a", "-type", "f"]
229+ <> concat [["-not", "-path", p </> "*"] | p <- toList allExclusions]
169230 where
170231 runFind args = do
171232 (_exitCode, out, err) <- readCreateProcessWithExitCode $ proc findExePath args
172233 putLog Debug $ T.strip $ T.pack err
173234 pure $ map T.unpack $ T.lines $ T.strip $ T.pack out
174235
236+-- | Calculates a set of root 'FilePath's along with each one's corresponding set of exclusions.
237+-- This is used when constructing a set of @find@ commands to run to produce a set of packages
238+-- that matches the user's @--interpret@/@--no-interpret@ settings.
239+calcIntepretFinds :: FilePath -> PathTree Interpret -> MMap.MonoidalMap FilePath (Set FilePath)
240+calcIntepretFinds treeRoot0 tree0 = runIdentity $ go treeRoot0 tree0
241+ where
242+ go treeRoot tree = foldPathTreeFor (== Interpret_Interpret) treeRoot tree $ \parent children -> do
243+ exclusions <- foldPathTreeFor (== Interpret_NoInterpret) parent children $ \parent' children' ->
244+ pure $ Map.singleton parent' children'
245+ deeperFinds <- Map.traverseWithKey go exclusions
246+ pure $ MMap.singleton parent (Map.keysSet exclusions) <> fold (MMap.MonoidalMap deeperFinds)
247+
248+-- | Traverses a 'PathTree' and folds all leaves matching a given predicate.
249+foldPathTreeFor
250+ :: forall m a b. (Applicative m, Monoid b)
251+ => (a -> Bool)
252+ -> FilePath
253+ -> PathTree a
254+ -> (FilePath -> PathTree a -> m b)
255+ -> m b
256+foldPathTreeFor predicate parent children f = case children of
257+ PathTree_Node (Just x) children' | predicate x -> f parent (PathTree_Node Nothing children')
258+ PathTree_Node _ children' -> fmap fold $ flip Map.traverseWithKey children' $ \k children'' ->
259+ foldPathTreeFor predicate (parent </> k) children'' f
260+
175261data GuessPackageFileError = GuessPackageFileError_Ambiguous [FilePath] | GuessPackageFileError_NotFound
176262 deriving (Eq, Ord, Show)
177263instance Exception GuessPackageFileError
@@ -272,46 +358,56 @@ parseCabalPackage' pkg = runExceptT $ do
272358 Left (_, errors) ->
273359 throwError $ T.pack $ "Failed to parse " <> packageFile <> ":\n" <> unlines (map show errors)
274360
275−parsePackagesOrFail :: MonadObelisk m => [FilePath] -> m (NE.NonEmpty CabalPackageInfo)
276−parsePackagesOrFail dirs = do
361+parsePackagesOrFail :: (MonadObelisk m, Foldable f) => f FilePath -> m (NE.NonEmpty CabalPackageInfo)
362+parsePackagesOrFail dirs' = do
277363 (pkgDirErrs, packageInfos') <- fmap partitionEithers $ for dirs $ \dir -> do
278364 flip fmap (parseCabalPackage dir) $ \case
279365 Just packageInfo
280366 | _cabalPackageInfo_buildable packageInfo -> Right packageInfo
281367 _ -> Left dir
282368
283− let packagesByName = Map.fromListWith (<>) [(_cabalPackageInfo_packageName p, p NE.:| []) | p <- packageInfos']
369+ -- Sort duplicate packages such that we prefer shorter paths, but fall back to alphabetical ordering.
370+ let packagesByName = Map.map (NE.sortBy $ comparing $ \p -> let n = _cabalPackageInfo_packageFile p in (length n, n))
371+ $ Map.fromListWith (<>) [(_cabalPackageInfo_packageName p, p NE.:| []) | p <- packageInfos']
284372 unambiguous <- ifor packagesByName $ \packageName ps -> case ps of
285373 p NE.:| [] -> pure p -- No ambiguity here
286374 p NE.:| _ -> do
287− putLog Warning $ T.pack $
288− "Packages named '" <> T.unpack packageName <> "' appear in " <> show (length ps) <> " different locations: "
289− <> intercalate ", " (map _cabalPackageInfo_packageFile $ toList ps)
290− <> "; Picking " <> _cabalPackageInfo_packageFile p
375+ let chosenText = " [Chosen] "
376+ prefix p'
377+ | _cabalPackageInfo_packageFile p' == _cabalPackageInfo_packageFile p = chosenText
378+ | otherwise = T.map (const ' ') chosenText
379+ putLog Warning $ T.unlines $
380+ "Packages named '" <> packageName <> "' appear in " <> T.pack (show $ length ps) <> " different locations: "
381+ : map (\p' -> prefix p' <> T.pack (_cabalPackageInfo_packageFile p')) (toList ps)
291382 pure p
292383
293384 packageInfos <- case NE.nonEmpty $ toList unambiguous of
294− Nothing -> failWith $ T.pack $ "No valid, buildable packages found in " <> intercalate ", " dirs
385+ Nothing -> failWith $ T.pack $
386+ "No valid, buildable packages found" <> (if null dirs then "" else " in " <> intercalate ", " dirs)
295387 Just xs -> pure xs
296388
297389 unless (null pkgDirErrs) $
298390 putLog Warning $ T.pack $ "Failed to find buildable packages in " <> intercalate ", " pkgDirErrs
299391
300392 pure packageInfos
393+ where
394+ dirs = toList dirs'
301395
302−packageInfoToNamePathMap :: [CabalPackageInfo] -> Map Text FilePath
303−packageInfoToNamePathMap = Map.fromList . map (_cabalPackageInfo_packageName &&& _cabalPackageInfo_packageRoot)
396+packageInfoToNamePathMap :: Foldable f => f CabalPackageInfo -> Map Text FilePath
397+packageInfoToNamePathMap = Map.fromList . map (_cabalPackageInfo_packageName &&& _cabalPackageInfo_packageRoot) . toList
304398
305399-- | Create ghci configuration to load the given packages
306400withGhciScript
307− :: MonadObelisk m
308− => [CabalPackageInfo] -- ^ List of packages to load into ghci
309− -> FilePath -- ^ All paths written to the .ghci file will be relative to this path
401+ :: (MonadObelisk m, Foldable f)
402+ => f CabalPackageInfo -- ^ List of packages to load into ghci
310403 -> (FilePath -> m ()) -- ^ Action to run with the path to generated temporary .ghci
311404 -> m ()
312−withGhciScript packageInfos pathBase f = do
313− selfExe <- liftIO getExecutablePath
314− let
405+withGhciScript (toList -> packageInfos) f =
406+ withSystemTempDirectory "ob-ghci" $ \fp -> do
407+ let dotGhciPath = fp </> ".ghci"
408+ liftIO $ writeFile dotGhciPath dotGhci
409+ f dotGhciPath
410+ where
315411 packageNames = Set.fromList $ map _cabalPackageInfo_packageName packageInfos
316412 modulesToLoad = mconcat
317413 [ [ "Obelisk.Run" | "obelisk-run" `Set.member` packageNames ]
@@ -319,66 +415,88 @@ withGhciScript packageInfos pathBase f = do
319415 , [ "Frontend" | "frontend" `Set.member` packageNames ]
320416 ]
321417 dotGhci = unlines
322− -- TODO: Shell escape
323− [ ":set -F -pgmF " <> selfExe <> " -optF " <> preprocessorIdentifier <> " " <> unwords (map (("-optF " <>) . makeRelative pathBase . _cabalPackageInfo_packageFile) packageInfos)
324− , ":set -i" <> intercalate ":" (packageInfos >>= rootedSourceDirs)
325− , if null modulesToLoad then "" else ":load " <> unwords modulesToLoad
418+ [ if null modulesToLoad then "" else ":load " <> unwords modulesToLoad
326419 , "import qualified Obelisk.Run"
327420 , "import qualified Frontend"
328− , "import qualified Backend" ]
329− withSystemTempDirectory "ob-ghci" $ \fp -> do
330− let dotGhciPath = fp </> ".ghci"
331− liftIO $ writeFile dotGhciPath dotGhci
332− f dotGhciPath
421+ , "import qualified Backend"
422+ ]
423+
424+-- | Builds a list of options to pass to ghci or set in .ghci file that configures
425+-- the preprocessor and source includes.
426+getGhciSessionSettings
427+ :: (MonadObelisk m, Foldable f)
428+ => f CabalPackageInfo -- ^ List of packages to load into ghci
429+ -> FilePath -- ^ All paths will be relative to this path
430+ -> Bool -- ^ Use relative paths
431+ -> m [String]
432+getGhciSessionSettings (toList -> packageInfos) pathBase useRelativePaths = do
433+ -- N.B. ghci settings do NOT support escaping in any way. To minimize the likelihood that
434+ -- paths-with-spaces ruin our day, we first canonicalize everything, and then relativize
435+ -- all paths to 'pathBase'.
436+ selfExe <- liftIO $ canonicalizePath =<< getExecutablePath
437+ canonicalPathBase <- liftIO $ canonicalizePath pathBase
438+
439+ (pkgFiles, pkgSrcPaths :: [NonEmpty FilePath]) <- fmap unzip $ liftIO $ for packageInfos $ \pkg -> do
440+ canonicalSrcDirs <- traverse canonicalizePath $ (_cabalPackageInfo_packageRoot pkg </>) <$> _cabalPackageInfo_sourceDirs pkg
441+ canonicalPkgFile <- canonicalizePath $ _cabalPackageInfo_packageFile pkg
442+ pure (canonicalPkgFile `relativeTo'` canonicalPathBase, (`relativeTo'` canonicalPathBase) <$> canonicalSrcDirs)
333443
444+ pure
445+ $ baseGhciOptions
446+ <> ["-F", "-pgmF", selfExe, "-optF", preprocessorIdentifier]
447+ <> concatMap (\p -> ["-optF", p]) pkgFiles
448+ <> [ "-i" <> intercalate ":" (concatMap toList pkgSrcPaths) ]
334449 where
335− rootedSourceDirs pkg = NE.toList $
336− makeRelative pathBase . (_cabalPackageInfo_packageRoot pkg </>) <$> _cabalPackageInfo_sourceDirs pkg
450+ relativeTo' = if useRelativePaths then relativeTo else const
451+
452+baseGhciOptions :: [String]
453+baseGhciOptions =
454+ [ "-ignore-dot-ghci"
455+ , "-no-user-package-db"
456+ , "-package-env", "-"
457+ ]
337458
338459-- | Run ghci repl
339460runGhciRepl
340− :: MonadObelisk m
461+ :: (MonadObelisk m, Foldable f)
341462 => FilePath -- ^ Path to project root
342− -> [CabalPackageInfo]
343− -> FilePath -- ^ Path to .ghci
463+ -> f CabalPackageInfo -- ^ Packages to keep unbuilt
464+ -> [String] -- ^ GHCi arguments
344465 -> m ()
345−runGhciRepl root packages dotGhci =
466+runGhciRepl root (toList -> packages) ghciArgs =
346467 -- NOTE: We do *not* want to use $(staticWhich "ghci") here because we need the
347468 -- ghc that is provided by the shell in the user's project.
348− nixShellWithPkgs root True False (packageInfoToNamePathMap packages) $ Just $ "ghci " <> makeBaseGhciOptions dotGhci -- TODO: Shell escape
469+ nixShellWithoutPkgs root True False (packageInfoToNamePathMap packages) "ghc" $
470+ Just $ unwords $ "ghci" : ghciArgs -- TODO: Shell escape
349471
350472-- | Run ghcid
351473runGhcid
352− :: MonadObelisk m
474+ :: (MonadObelisk m, Foldable f)
353475 => FilePath -- ^ Path to project root
354476 -> Bool -- ^ Should we chdir to root when running this process?
355− -> FilePath -- ^ Path to .ghci
356− -> [CabalPackageInfo]
477+ -> [String] -- ^ GHCi arguments
478+ -> f CabalPackageInfo -- ^ Packages to keep unbuilt
357479 -> Maybe String -- ^ Optional command to run at every reload
358480 -> m ()
359−runGhcid root chdirToRoot dotGhci packages mcmd =
360− nixShellWithPkgs root True chdirToRoot (packageInfoToNamePathMap packages) (Just $ unwords $ ghcidExePath : opts) -- TODO: Shell escape
481+runGhcid root chdirToRoot ghciArgs (toList -> packages) mcmd =
482+ nixShellWithoutPkgs root True chdirToRoot (packageInfoToNamePathMap packages) "ghc" $
483+ Just $ unwords $ ghcidExePath : opts -- TODO: Shell escape
361484 where
362485 opts =
363486 [ "-W"
364− , "--command='ghci -ignore-dot-ghci " <> makeBaseGhciOptions dotGhci <> "' "
365487 , "--outputfile=ghcid-output.txt"
366488 ] <> map (\x -> "--reload='" <> x <> "'") reloadFiles
367489 <> map (\x -> "--restart='" <> x <> "'") restartFiles
368490 <> testCmd
491+ <> ["--command='" <> unwords ("ghci" : ghciArgs) <> "'"] -- TODO: Shell escape
369492 testCmd = maybeToList (flip fmap mcmd $ \cmd -> "--test='" <> cmd <> "'") -- TODO: Shell escape
370493
371494 adjustRoot x = if chdirToRoot then makeRelative root x else x
372495 reloadFiles = map adjustRoot [root </> "config"]
373496 restartFiles = map (adjustRoot . _cabalPackageInfo_packageFile) packages
374497
375−makeBaseGhciOptions :: FilePath -> String
376−makeBaseGhciOptions dotGhci =
377− unwords
378− [ "-no-user-package-db"
379− , "-package-env -"
380− , "-ghci-script " <> dotGhci
381− ]
498+mkGhciScriptArg :: FilePath -> [String]
499+mkGhciScriptArg dotGhci = ["-ghci-script", dotGhci]
382500
383501getFreePort :: MonadIO m => m PortNumber
384502getFreePort = liftIO $ withSocketsDo $ do
@@ -389,3 +507,37 @@ getFreePort = liftIO $ withSocketsDo $ do
389507 sock <- socket (addrFamily addr) (addrSocketType addr) (addrProtocol addr)
390508 bind sock (addrAddress addr)
391509 return sock
510+
511+
512+-- | Convert a 'FilePath' into a 'PathTree'.
513+pathToTree :: a -> FilePath -> PathTree a
514+pathToTree a p = go $ splitDirectories p
515+ where
516+ go [] = PathTree_Node (Just a) mempty
517+ go (x : xs) = PathTree_Node Nothing $ Map.singleton x $ go xs
518+
519+-- | Like 'zipWith' but pads with a padding value instead of stopping on the shortest list.
520+zipDefaultWith :: a -> b -> (a -> b -> c) -> [a] -> [b] -> [c]
521+zipDefaultWith _da _db _f [] [] = []
522+zipDefaultWith da db f (a:as) [] = f a db : zipDefaultWith da db f as []
523+zipDefaultWith da db f [] (b:bs) = f da b : zipDefaultWith da db f [] bs
524+zipDefaultWith da db f (a:as) (b:bs) = f a b : zipDefaultWith da db f as bs
525+
526+-- | Makes the first absolute path relative to the second absolute path.
527+--
528+-- Both input paths MUST be absolute.
529+--
530+-- Unlike 'makeRelative' this does not merely strip prefixes. It will introduce
531+-- enough @..@ paths to make the resulting path truly relative in virtually every
532+-- case. The only exception is on Windows when the two paths are on different
533+-- drives. In this case the resulting path may be absolute.
534+relativeTo :: FilePath -> FilePath -> FilePath
535+relativeTo dir base
536+ = bifoldr1 (</>)
537+ $ bimap (collapse . (".." <$) . catMaybes) (collapse . catMaybes)
538+ $ unzip
539+ $ dropWhile (\(a,b) -> a == b)
540+ $ zipDefaultWith Nothing Nothing (,)
541+ (map Just $ splitDirectories base)
542+ (map Just $ splitDirectories dir)
543+ where collapse = foldr (</>) ""
lib/command/src/Obelisk/Command/Thunk.hs+396−277
@@ -5,67 +5,48 @@
55{-# LANGUAGE MultiWayIf #-}
66{-# LANGUAGE OverloadedStrings #-}
77{-# LANGUAGE PatternGuards #-}
8+{-# LANGUAGE QuasiQuotes #-}
89{-# LANGUAGE ScopedTypeVariables #-}
910{-# LANGUAGE TypeApplications #-}
10−module Obelisk.Command.Thunk
11− ( GitHubSource (..)
12− , GitUri (..)
13− , ReadThunkError (..)
14− , ThunkConfig (..)
15− , ThunkData (..)
16− , ThunkPackConfig (..)
17− , ThunkPtr (..)
18− , ThunkRev (..)
19− , ThunkSource (..)
20− , ThunkUpdateConfig (..)
21− , attrCacheFileName
22− , createThunk
23− , createThunkWithLatest
24− , getLatestRev
25− , getThunkGitBranch
26− , getThunkPtr
27− , gitUriToText
28− , nixBuildAttrWithCache
29− , nixBuildThunkAttrWithCache
30− , packThunk
31− , parseGitUri
32− , readThunk
33− , unpackThunk
34− , updateThunk
35− , updateThunkToLatest
36− , uriThunkPtr
37− ) where
11+module Obelisk.Command.Thunk where
3812
3913import Control.Applicative
4014import Control.Exception (displayException, try)
41−import qualified Control.Lens as Lens
42−import Control.Lens.Indexed hiding ((<.>))
15+import Control.Lens (ifor, ifor_, (.~))
4316import Control.Monad
44−import Control.Monad.Catch (handle)
17+import Control.Monad.Extra (findM)
18+import Control.Monad.Catch (MonadCatch, handle)
4519import Control.Monad.Except
4620import Data.Aeson ((.=))
4721import qualified Data.Aeson as Aeson
4822import Data.Aeson.Encode.Pretty
4923import qualified Data.Aeson.Types as Aeson
24+import Data.Bifunctor (first)
5025import qualified Data.ByteString.Lazy as LBS
5126import Data.Containers.ListUtils (nubOrd)
5227import Data.Default
5328import Data.Either.Combinators (fromRight', rightToMaybe)
29+import Data.Foldable (toList)
30+import Data.Function ((&))
31+import Data.Functor ((<&>))
5432import Data.Git.Ref (Ref)
5533import qualified Data.Git.Ref as Ref
5634import qualified Data.List as L
57−import Data.List.NonEmpty (NonEmpty (..))
35+import Data.List.NonEmpty (NonEmpty (..), nonEmpty)
5836import qualified Data.List.NonEmpty as NonEmpty
5937import Data.Map (Map)
6038import qualified Data.Map as Map
6139import Data.Maybe
6240import Data.Set (Set)
6341import qualified Data.Set as Set
42+import Data.String.Here.Interpolated (i)
43+import Data.String.Here.Uninterpolated (here)
6444import Data.Text (Text)
6545import qualified Data.Text as T
6646import Data.Text.Encoding
6747import qualified Data.Text.IO as T
6848import Data.Time.Clock.POSIX (posixSecondsToUTCTime)
49+import Data.Traversable (for)
6950import Data.Yaml (parseMaybe)
7051import GitHub
7152import GitHub.Data.Name
@@ -84,11 +65,10 @@ import Obelisk.Command.Utils
8465
8566--TODO: Support symlinked thunk data
8667data ThunkData
87− = ThunkData_Packed ThunkPtr
68+ = ThunkData_Packed ThunkSpec ThunkPtr
8869 -- ^ Packed thunk
89− | ThunkData_Checkout (Maybe ThunkPtr)
70+ | ThunkData_Checkout
9071 -- ^ Checked out thunk that was unpacked from this pointer
91− deriving (Show, Eq, Ord)
9272
9373-- | A reference to the exact data that a thunk should translate into
9474data ThunkPtr = ThunkPtr
@@ -114,6 +94,11 @@ data ThunkSource
11494 | ThunkSource_Git GitSource
11595 deriving (Show, Eq, Ord)
11696
97+thunkSourceToGitSource :: ThunkSource -> GitSource
98+thunkSourceToGitSource = \case
99+ ThunkSource_GitHub s -> forgetGithub False s
100+ ThunkSource_Git s -> s
101+
117102data GitHubSource = GitHubSource
118103 { _gitHubSource_owner :: Name Owner
119104 , _gitHubSource_repo :: Name Repo
@@ -213,96 +198,117 @@ nixPrefetchGit uri rev fetchSubmodules =
213198
214199--TODO: Pretty print these
215200data ReadThunkError
216− = ReadThunkError_UnrecognizedFiles
217− | ReadThunkError_AmbiguousFiles
218− | ReadThunkError_UnrecognizedLoader Text
219− | ReadThunkError_UnparseablePtr LBS.ByteString
220− deriving (Show)
221−
222−-- | Read a thunk and validate that it is only a thunk, either packed or unpacked.
223−-- If the thunk is packed and additional data is present, fail.
224−readThunk :: MonadObelisk m => FilePath -> m (Either ReadThunkError ThunkData)
225−readThunk thunkDir = do
226− files <- liftIO $ listDirectory thunkDir
227− case ".git" `elem` files of
228− True -> fmap ThunkData_Checkout <$> do
229− let origThunkPath = thunkDir </> ".git" </> "obelisk" </> "orig-thunk"
230− liftIO $ doesDirectoryExist origThunkPath >>= \case
231− True -> fmap Just <$> readPackedThunk origThunkPath
232− False -> return $ return Nothing
233− False -> liftIO $ fmap ThunkData_Packed <$> readPackedThunk thunkDir
201+ = ReadThunkError_UnrecognizedThunk
202+ | ReadThunkError_UnrecognizedPaths (NonEmpty FilePath)
203+ | ReadThunkError_MissingPaths (NonEmpty FilePath)
204+ | ReadThunkError_UnparseablePtr FilePath String
205+ | ReadThunkError_FileError IOError
206+ | ReadThunkError_FileDoesNotMatch FilePath Text
207+ | ReadThunkError_UnrecognizedState String
208+ | ReadThunkError_AmbiguousPackedState ThunkPtr ThunkPtr
209+ deriving (Show)
210+
211+unpackedDirName :: FilePath
212+unpackedDirName = "."
234213
235214attrCacheFileName :: FilePath
236215attrCacheFileName = ".attr-cache"
237216
238−data ThunkType = ThunkType
239− { _thunkType_loader :: FilePath
240− , _thunkType_json :: FilePath
241− , _thunkType_optional :: Set FilePath
242− , _thunkType_loaderVersions :: NonEmpty Text
243− , _thunkType_parser :: Aeson.Object -> Aeson.Parser ThunkPtr
244− }
245−
246−gitHubThunkType :: ThunkType
247−gitHubThunkType = ThunkType
248− { _thunkType_loader = "default.nix"
249− , _thunkType_json = "github.json"
250− , _thunkType_optional = Set.fromList [attrCacheFileName]
251− , _thunkType_loaderVersions = gitHubStandaloneLoaders
252− , _thunkType_parser = parseThunkPtr $ \v ->
253− ThunkSource_GitHub <$> parseGitHubSource v <|> ThunkSource_Git <$> parseGitSource v
254− }
255−
256−gitThunkType :: ThunkType
257−gitThunkType = ThunkType
258− { _thunkType_loader = "default.nix"
259− , _thunkType_json = "git.json"
260− , _thunkType_optional = Set.fromList [attrCacheFileName]
261− , _thunkType_loaderVersions = plainGitStandaloneLoaders
262− , _thunkType_parser = parseThunkPtr $ fmap ThunkSource_Git . parseGitSource
217+-- | Specification for how a file in a thunk version works.
218+data ThunkFileSpec
219+ = ThunkFileSpec_Ptr (LBS.ByteString -> Either String ThunkPtr) -- ^ This file specifies 'ThunkPtr' data
220+ | ThunkFileSpec_FileMatches Text -- ^ This file must match the given content exactly
221+ | ThunkFileSpec_CheckoutIndicator -- ^ Existence of this directory indicates that the thunk is unpacked
222+ | ThunkFileSpec_AttrCache -- ^ This directory is an attribute cache
223+
224+-- | Specification for how a set of files in a thunk version work.
225+data ThunkSpec = ThunkSpec
226+ { _thunkSpec_name :: !Text
227+ , _thunkSpec_files :: !(Map FilePath ThunkFileSpec)
263228 }
264229
265−thunkTypes :: [ThunkType]
266−thunkTypes = [gitThunkType, gitHubThunkType]
267−
268−findThunkType :: [ThunkType] -> FilePath -> IO (Either ReadThunkError ThunkType)
269−findThunkType types thunkDir = do
270− matches <- fmap catMaybes $ forM types $ \thunkType -> do
271− let
272− expectedContents = Set.fromList $ (thunkDir </>) <$>
273− [ _thunkType_loader thunkType
274− , _thunkType_json thunkType
275− ]
276− optionalContents = Set.map (thunkDir </>) (_thunkType_optional thunkType)
277−
278− -- Ensure that there aren't any other files in the thunk
279− -- NB: System.Directory.listDirectory returns the contents without the directory path
280− files <- liftIO $ Set.fromList . fmap (thunkDir </>) <$> listDirectory thunkDir
281− let unexpectedContents = files `Set.difference` (expectedContents `Set.union` optionalContents)
282− missingContents = expectedContents `Set.difference` files
283− pure $ if Set.null unexpectedContents && Set.null missingContents
284− then Just thunkType
285− else Nothing
286−
287− pure $ case matches of
288− [singleMatch] -> Right singleMatch
289− [] -> Left ReadThunkError_UnrecognizedFiles
290− _ -> Left ReadThunkError_AmbiguousFiles
230+thunkSpecTypes :: NonEmpty (NonEmpty ThunkSpec)
231+thunkSpecTypes = gitThunkSpecs :| [gitHubThunkSpecs]
232+
233+-- | Attempts to match a 'ThunkSpec' to a given directory.
234+matchThunkSpecToDir
235+ :: (MonadError ReadThunkError m, MonadIO m, MonadCatch m)
236+ => ThunkSpec -- ^ 'ThunkSpec' to match against the given files/directory
237+ -> FilePath -- ^ Path to directory
238+ -> Set FilePath -- ^ Set of file paths relative to the given directory
239+ -> m ThunkData
240+matchThunkSpecToDir thunkSpec dir dirFiles = do
241+ case nonEmpty (toList $ dirFiles `Set.difference` expectedPaths) of
242+ Just fs -> throwError $ ReadThunkError_UnrecognizedPaths $ (dir </>) <$> fs
243+ Nothing -> pure ()
244+ case nonEmpty (toList $ requiredPaths `Set.difference` dirFiles) of
245+ Just fs -> throwError $ ReadThunkError_MissingPaths $ (dir </>) <$> fs
246+ Nothing -> pure ()
247+ datas <- fmap toList $ flip Map.traverseMaybeWithKey (_thunkSpec_files thunkSpec) $ \expectedPath -> \case
248+ ThunkFileSpec_AttrCache -> Nothing <$ dirMayExist expectedPath
249+ ThunkFileSpec_CheckoutIndicator -> liftIO (doesDirectoryExist (dir </> expectedPath)) <&> \case
250+ False -> Nothing
251+ True -> Just ThunkData_Checkout
252+ ThunkFileSpec_FileMatches expectedContents -> handle (\(e :: IOError) -> throwError $ ReadThunkError_FileError e) $ do
253+ actualContents <- liftIO (T.readFile $ dir </> expectedPath)
254+ case T.strip expectedContents == T.strip actualContents of
255+ True -> pure Nothing
256+ False -> throwError $ ReadThunkError_FileDoesNotMatch (dir </> expectedPath) expectedContents
257+ ThunkFileSpec_Ptr parser -> handle (\(e :: IOError) -> throwError $ ReadThunkError_FileError e) $ do
258+ let path = dir </> expectedPath
259+ liftIO (doesFileExist path) >>= \case
260+ False -> pure Nothing
261+ True -> do
262+ actualContents <- liftIO $ LBS.readFile path
263+ case parser actualContents of
264+ Right v -> pure $ Just (ThunkData_Packed thunkSpec v)
265+ Left e -> throwError $ ReadThunkError_UnparseablePtr (dir </> expectedPath) e
266+
267+ case nonEmpty datas of
268+ Nothing -> throwError ReadThunkError_UnrecognizedThunk
269+ Just xs -> fold1WithM xs $ \a b -> either throwError pure (mergeThunkData a b)
270+ where
271+ rootPathsOnly = Set.fromList . mapMaybe takeRootDir . Map.keys
272+ takeRootDir = fmap NonEmpty.head . nonEmpty . splitPath
273+
274+ expectedPaths = rootPathsOnly $ _thunkSpec_files thunkSpec
275+
276+ requiredPaths = rootPathsOnly $ Map.filter isRequiredFileSpec $ _thunkSpec_files thunkSpec
277+ isRequiredFileSpec = \case
278+ ThunkFileSpec_FileMatches _ -> True
279+ _ -> False
280+
281+ dirMayExist expectedPath = liftIO (doesFileExist (dir </> expectedPath)) >>= \case
282+ True -> throwError $ ReadThunkError_UnrecognizedPaths $ expectedPath :| []
283+ False -> pure ()
284+
285+ -- Combine 'ThunkData' from different files, preferring "Checkout" over "Packed"
286+ mergeThunkData ThunkData_Checkout ThunkData_Checkout = Right ThunkData_Checkout
287+ mergeThunkData ThunkData_Checkout ThunkData_Packed{} = Left bothPackedAndUnpacked
288+ mergeThunkData ThunkData_Packed{} ThunkData_Checkout = Left bothPackedAndUnpacked
289+ mergeThunkData a@(ThunkData_Packed _ ptrA) (ThunkData_Packed _ ptrB) =
290+ if ptrA == ptrB then Right a else Left $ ReadThunkError_AmbiguousPackedState ptrA ptrB
291+
292+ bothPackedAndUnpacked = ReadThunkError_UnrecognizedState "Both packed data and checkout present"
293+
294+ fold1WithM (x :| xs) f = foldM f x xs
295+
296+readThunkWith
297+ :: (MonadObelisk m)
298+ => NonEmpty (NonEmpty ThunkSpec) -> FilePath -> m (Either ReadThunkError ThunkData)
299+readThunkWith specTypes dir = do
300+ dirFiles <- Set.fromList <$> liftIO (listDirectory dir)
301+ let specs = concatMap toList $ toList $ NonEmpty.transpose specTypes -- Interleave spec types so we try each one in a "fair" ordering
302+ flip fix specs $ \loop -> \case
303+ [] -> pure $ Left ReadThunkError_UnrecognizedThunk
304+ spec:rest -> runExceptT (matchThunkSpecToDir spec dir dirFiles) >>= \case
305+ Left e -> putLog Debug [i|Thunk specification ${_thunkSpec_name spec} did not match ${dir}: ${e}|] *> loop rest
306+ x@(Right _) -> x <$ putLog Debug [i|Thunk specification ${_thunkSpec_name spec} matched ${dir}|]
291307
292308-- | Read a thunk and validate that it is exactly a packed thunk.
293309-- If additional data is present, fail.
294−readPackedThunk :: FilePath -> IO (Either ReadThunkError ThunkPtr)
295−readPackedThunk thunkDir = runExceptT $ do
296− thunkType <- ExceptT $ findThunkType thunkTypes thunkDir
297− -- Ensure that we recognize the thunk loader
298− loader <- liftIO $ T.readFile $ thunkDir </> _thunkType_loader thunkType
299− unless (loader `elem` _thunkType_loaderVersions thunkType) $ do
300− throwError $ ReadThunkError_UnrecognizedLoader loader
301−
302− txt <- liftIO $ LBS.readFile $ thunkDir </> _thunkType_json thunkType
303− case parseMaybe (_thunkType_parser thunkType) =<< Aeson.decode txt of
304− Nothing -> throwError $ ReadThunkError_UnparseablePtr txt
305− Just ptr -> return ptr
310+readThunk :: (MonadObelisk m) => FilePath -> m (Either ReadThunkError ThunkData)
311+readThunk = readThunkWith thunkSpecTypes
306312
307313parseThunkPtr :: (Aeson.Object -> Aeson.Parser ThunkSource) -> Aeson.Object -> Aeson.Parser ThunkPtr
308314parseThunkPtr parseSrc v = do
@@ -346,16 +352,18 @@ parseGitSource v = do
346352overwriteThunk :: MonadObelisk m => FilePath -> ThunkPtr -> m ()
347353overwriteThunk target thunk = do
348354 -- Ensure that this directory is a valid thunk (i.e. so we aren't losing any data)
349− Right _ <- readThunk target
355+ readThunk target >>= \case
356+ Left e -> failWith [i|Invalid thunk at ${target}: ${e}|]
357+ Right _ -> pure ()
350358
351359 --TODO: Is there a safer way to do this overwriting?
352− liftIO $ removeDirectoryRecursive target
353− liftIO $ createThunk target thunk
360+ liftIO $ removePathForcibly target
361+ createThunk target $ Right thunk
354362
355−thunkPtrLoader :: ThunkPtr -> Text
356−thunkPtrLoader thunk = case _thunkPtr_source thunk of
357− ThunkSource_GitHub _ -> NonEmpty.head gitHubStandaloneLoaders
358− ThunkSource_Git _ -> NonEmpty.head plainGitStandaloneLoaders
363+thunkPtrToSpec :: ThunkPtr -> ThunkSpec
364+thunkPtrToSpec thunk = case _thunkPtr_source thunk of
365+ ThunkSource_GitHub _ -> NonEmpty.head gitHubThunkSpecs
366+ ThunkSource_Git _ -> NonEmpty.head gitThunkSpecs
359367
360368-- It's important that formatting be very consistent here, because
361369-- otherwise when people update thunks, their patches will be messy
@@ -402,35 +410,41 @@ encodeThunkPtrData (ThunkPtr rev src) = case src of
402410 , confTrailingNewline = True
403411 }
404412
405−createThunk :: MonadIO m => FilePath -> ThunkPtr -> m ()
406−createThunk target thunk = liftIO $ do
407− createDirectoryIfMissing True (target </> attrCacheFileName)
408− T.writeFile (target </> "default.nix") (thunkPtrLoader thunk)
409− let
410− jsonFileName = case _thunkPtr_source thunk of
411− ThunkSource_GitHub _ -> "github"
412− ThunkSource_Git _ -> "git"
413− LBS.writeFile (target </> jsonFileName <.> "json") (encodeThunkPtrData thunk)
413+createThunk :: MonadObelisk m => FilePath -> Either ThunkSpec ThunkPtr -> m ()
414+createThunk target ptrInfo =
415+ ifor_ (_thunkSpec_files spec) $ \path -> \case
416+ ThunkFileSpec_FileMatches content -> withReadyPath path $ \p -> liftIO $ T.writeFile p content
417+ ThunkFileSpec_Ptr _ -> case ptrInfo of
418+ Left _ -> pure () -- We can't write the ptr without it
419+ Right ptr -> withReadyPath path $ \p -> liftIO $ LBS.writeFile p (encodeThunkPtrData ptr)
420+ _ -> pure ()
421+ where
422+ spec = either id thunkPtrToSpec ptrInfo
423+ withReadyPath path f = do
424+ let fullPath = target </> path
425+ putLog Debug $ "Writing thunk file " <> T.pack fullPath
426+ liftIO $ createDirectoryIfMissing True $ takeDirectory fullPath
427+ f fullPath
414428
415429createThunkWithLatest :: MonadObelisk m => FilePath -> ThunkSource -> m ()
416430createThunkWithLatest target s = do
417431 rev <- getLatestRev s
418− createThunk target $ ThunkPtr
432+ createThunk target $ Right $ ThunkPtr
419433 { _thunkPtr_source = s
420434 , _thunkPtr_rev = rev
421435 }
422436
423437updateThunkToLatest :: MonadObelisk m => ThunkUpdateConfig -> FilePath -> m ()
424−updateThunkToLatest (ThunkUpdateConfig mBranch thunkConfig) target = withSpinner' ("Updating thunk " <> T.pack target <> " to latest") (pure $ const $ "Thunk " <> T.pack target <> " updated to latest") $ do
425− checkThunkDirectory "ob thunk update directory cannot be '.'" target
438+updateThunkToLatest (ThunkUpdateConfig mBranch thunkConfig) target = spinner $ do
439+ checkThunkDirectory target
426440 -- check to see if thunk should be updated to a specific branch or just update it's current branch
427441 case mBranch of
428442 Nothing -> do
429443 (overwrite, ptr) <- readThunk target >>= \case
430− Left err -> failWith $ T.pack $ "thunk update: " <> show err
444+ Left err -> failWith [i|Thunk update: ${err}|]
431445 Right c -> case c of
432− ThunkData_Packed t -> return (target, t)
433− ThunkData_Checkout _ -> failWith "cannot update an unpacked thunk"
446+ ThunkData_Packed _ t -> return (target, t)
447+ ThunkData_Checkout -> failWith "cannot update an unpacked thunk"
434448 let src = _thunkPtr_source ptr
435449 rev <- getLatestRev src
436450 overwriteThunk overwrite $ modifyThunkPtrByConfig thunkConfig $ ThunkPtr
@@ -438,14 +452,12 @@ updateThunkToLatest (ThunkUpdateConfig mBranch thunkConfig) target = withSpinner
438452 , _thunkPtr_rev = rev
439453 }
440454 Just branch -> readThunk target >>= \case
441− Left err -> failWith $ T.pack $ "thunk update: " <> show err
455+ Left err -> failWith [i|Thunk update: ${err}|]
442456 Right c -> case c of
443− ThunkData_Packed t -> case _thunkPtr_source t of
444− ThunkSource_Git tsg -> setThunk thunkConfig target tsg branch
445− ThunkSource_GitHub tsgh -> do
446− let tsg = forgetGithub False tsgh
447− setThunk thunkConfig target tsg branch
448− ThunkData_Checkout _ -> failWith $ T.pack $ "thunk located at " <> show target <> " is unpacked. Use ob thunk pack on the desired directory and then try ob thunk update again."
457+ ThunkData_Packed _ t -> setThunk thunkConfig target (thunkSourceToGitSource $ _thunkPtr_source t) branch
458+ ThunkData_Checkout -> failWith [i|Thunk located at ${target} is unpacked. Use 'ob thunk pack' on the desired directory and then try 'ob thunk update' again.|]
459+ where
460+ spinner = withSpinner' ("Updating thunk " <> T.pack target <> " to latest") (pure $ const $ "Thunk " <> T.pack target <> " updated to latest")
449461
450462setThunk :: MonadObelisk m => ThunkConfig -> FilePath -> GitSource -> String -> m ()
451463setThunk thunkConfig target gs branch = do
@@ -455,21 +467,21 @@ setThunk thunkConfig target gs branch = do
455467
456468-- | All recognized github standalone loaders, ordered from newest to oldest.
457469-- This tool will only ever produce the newest one when it writes a thunk.
458−gitHubStandaloneLoaders :: NonEmpty Text
459−gitHubStandaloneLoaders =
460− gitHubStandaloneLoaderV4 :|
461− [ gitHubStandaloneLoaderV3
462− , gitHubStandaloneLoaderV2
463− , gitHubStandaloneLoaderV1
470+gitHubThunkSpecs :: NonEmpty ThunkSpec
471+gitHubThunkSpecs =
472+ gitHubThunkSpecV5 :|
473+ [ gitHubThunkSpecV4
474+ , gitHubThunkSpecV3
475+ , gitHubThunkSpecV2
476+ , gitHubThunkSpecV1
464477 ]
465478
466−gitHubStandaloneLoaderV1 :: Text
467−gitHubStandaloneLoaderV1 = T.unlines
468− [ "import ((import <nixpkgs> {}).fetchFromGitHub (builtins.fromJSON (builtins.readFile ./github.json)))"
469− ]
479+gitHubThunkSpecV1 :: ThunkSpec
480+gitHubThunkSpecV1 = legacyGitHubThunkSpec "github-v1"
481+ "import ((import <nixpkgs> {}).fetchFromGitHub (builtins.fromJSON (builtins.readFile ./github.json)))"
470482
471−gitHubStandaloneLoaderV2 :: Text
472−gitHubStandaloneLoaderV2 = T.unlines
483+gitHubThunkSpecV2 :: ThunkSpec
484+gitHubThunkSpecV2 = legacyGitHubThunkSpec "github-v2" $ T.unlines
473485 [ "# DO NOT HAND-EDIT THIS FILE" --TODO: Add something about how to get more info on Obelisk, etc.
474486 , "import ((import <nixpkgs> {}).fetchFromGitHub ("
475487 , " let json = builtins.fromJSON (builtins.readFile ./github.json);"
@@ -479,8 +491,8 @@ gitHubStandaloneLoaderV2 = T.unlines
479491 , "))"
480492 ]
481493
482−gitHubStandaloneLoaderV3 :: Text
483−gitHubStandaloneLoaderV3 = T.unlines
494+gitHubThunkSpecV3 :: ThunkSpec
495+gitHubThunkSpecV3 = legacyGitHubThunkSpec "github-v3" $ T.unlines
484496 [ "# DO NOT HAND-EDIT THIS FILE"
485497 , "let"
486498 , " fetch = { private ? false, ... }@args: if private && builtins.hasAttr \"fetchGit\" builtins"
@@ -499,8 +511,8 @@ gitHubStandaloneLoaderV3 = T.unlines
499511 , "in import (fetch (builtins.fromJSON (builtins.readFile ./github.json)))"
500512 ]
501513
502−gitHubStandaloneLoaderV4 :: Text
503−gitHubStandaloneLoaderV4 = T.unlines
514+gitHubThunkSpecV4 :: ThunkSpec
515+gitHubThunkSpecV4 = legacyGitHubThunkSpec "github-v4" $ T.unlines
504516 [ "# DO NOT HAND-EDIT THIS FILE"
505517 , "let fetch = { private ? false, fetchSubmodules ? false, owner, repo, rev, sha256, ... }:"
506518 , " if !fetchSubmodules && !private then builtins.fetchTarball {"
@@ -511,24 +523,50 @@ gitHubStandaloneLoaderV4 = T.unlines
511523 , "in import (fetch (builtins.fromJSON (builtins.readFile ./github.json)))"
512524 ]
513525
514−plainGitStandaloneLoaders :: NonEmpty Text
515−plainGitStandaloneLoaders =
516− plainGitStandaloneLoaderV4 :|
517− [ plainGitStandaloneLoaderV1
518− , plainGitStandaloneLoaderV2
519− , plainGitStandaloneLoaderV3
526+legacyGitHubThunkSpec :: Text -> Text -> ThunkSpec
527+legacyGitHubThunkSpec name loader = ThunkSpec name $ Map.fromList
528+ [ ("default.nix", ThunkFileSpec_FileMatches $ T.strip loader)
529+ , ("github.json" , ThunkFileSpec_Ptr parseGitHubJsonBytes)
530+ , (attrCacheFileName, ThunkFileSpec_AttrCache)
531+ , (".git", ThunkFileSpec_CheckoutIndicator)
520532 ]
521533
522−plainGitStandaloneLoaderV1 :: Text
523−plainGitStandaloneLoaderV1 = T.unlines
534+gitHubThunkSpecV5 :: ThunkSpec
535+gitHubThunkSpecV5 = mkThunkSpec "github-v5" "github.json" parseGitHubJsonBytes [here|
536+# DO NOT HAND-EDIT THIS FILE
537+let fetch = { private ? false, fetchSubmodules ? false, owner, repo, rev, sha256, ... }:
538+ if !fetchSubmodules && !private then builtins.fetchTarball {
539+ url = "https://github.com/${owner}/${repo}/archive/${rev}.tar.gz"; inherit sha256;
540+ } else (import <nixpkgs> {}).fetchFromGitHub {
541+ inherit owner repo rev sha256 fetchSubmodules private;
542+ };
543+ json = builtins.fromJSON (builtins.readFile ./github.json);
544+in fetch json
545+|]
546+
547+parseGitHubJsonBytes :: LBS.ByteString -> Either String ThunkPtr
548+parseGitHubJsonBytes = parseJsonObject $ parseThunkPtr $ \v ->
549+ ThunkSource_GitHub <$> parseGitHubSource v <|> ThunkSource_Git <$> parseGitSource v
550+
551+gitThunkSpecs :: NonEmpty ThunkSpec
552+gitThunkSpecs =
553+ gitThunkSpecV5 :|
554+ [ gitThunkSpecV4
555+ , gitThunkSpecV3
556+ , gitThunkSpecV2
557+ , gitThunkSpecV1
558+ ]
559+
560+gitThunkSpecV1 :: ThunkSpec
561+gitThunkSpecV1 = legacyGitThunkSpec "git-v1" $ T.unlines
524562 [ "# DO NOT HAND-EDIT THIS FILE"
525563 , "let fetchGit = {url, rev, ref ? null, branch ? null, sha256 ? null, fetchSubmodules ? null}:"
526564 , " assert !fetchSubmodules; (import <nixpkgs> {}).fetchgit { inherit url rev sha256; };"
527565 , "in import (fetchGit (builtins.fromJSON (builtins.readFile ./git.json)))"
528566 ]
529567
530−plainGitStandaloneLoaderV2 :: Text
531−plainGitStandaloneLoaderV2 = T.unlines
568+gitThunkSpecV2 :: ThunkSpec
569+gitThunkSpecV2 = legacyGitThunkSpec "git-v2" $ T.unlines
532570 [ "# DO NOT HAND-EDIT THIS FILE"
533571 , "let fetchGit = {url, rev, ref ? null, branch ? null, sha256 ? null, fetchSubmodules ? null}:"
534572 , " if builtins.hasAttr \"fetchGit\" builtins"
@@ -539,8 +577,8 @@ plainGitStandaloneLoaderV2 = T.unlines
539577
540578-- This loader has a bug because @builtins.fetchGit@ is not given a @ref@
541579-- and will fail to find commits without this because it does shallow clones.
542−plainGitStandaloneLoaderV3 :: Text
543−plainGitStandaloneLoaderV3 = T.unlines
580+gitThunkSpecV3 :: ThunkSpec
581+gitThunkSpecV3 = legacyGitThunkSpec "git-v3" $ T.unlines
544582 [ "# DO NOT HAND-EDIT THIS FILE"
545583 , "let fetch = {url, rev, ref ? null, sha256 ? null, fetchSubmodules ? false, private ? false, ...}:"
546584 , " let realUrl = let firstChar = builtins.substring 0 1 url; in"
@@ -555,8 +593,8 @@ plainGitStandaloneLoaderV3 = T.unlines
555593 , "in import (fetch (builtins.fromJSON (builtins.readFile ./git.json)))"
556594 ]
557595
558−plainGitStandaloneLoaderV4 :: Text
559−plainGitStandaloneLoaderV4 = T.unlines
596+gitThunkSpecV4 :: ThunkSpec
597+gitThunkSpecV4 = legacyGitThunkSpec "git-v4" $ T.unlines
560598 [ "# DO NOT HAND-EDIT THIS FILE"
561599 , "let fetch = {url, rev, branch ? null, sha256 ? null, fetchSubmodules ? false, private ? false, ...}:"
562600 , " let realUrl = let firstChar = builtins.substring 0 1 url; in"
@@ -572,10 +610,52 @@ plainGitStandaloneLoaderV4 = T.unlines
572610 , "in import (fetch (builtins.fromJSON (builtins.readFile ./git.json)))"
573611 ]
574612
575−thunkFileNames :: [FilePath]
576−thunkFileNames = ["default.nix", "github.json", "git.json"]
613+legacyGitThunkSpec :: Text -> Text -> ThunkSpec
614+legacyGitThunkSpec name loader = ThunkSpec name $ Map.fromList
615+ [ ("default.nix", ThunkFileSpec_FileMatches $ T.strip loader)
616+ , ("git.json" , ThunkFileSpec_Ptr parseGitJsonBytes)
617+ , (attrCacheFileName, ThunkFileSpec_AttrCache)
618+ , (".git", ThunkFileSpec_CheckoutIndicator)
619+ ]
620+
621+gitThunkSpecV5 :: ThunkSpec
622+gitThunkSpecV5 = mkThunkSpec "git-v5" "git.json" parseGitJsonBytes [here|
623+# DO NOT HAND-EDIT THIS FILE
624+let fetch = {url, rev, branch ? null, sha256 ? null, fetchSubmodules ? false, private ? false, ...}:
625+ let realUrl = let firstChar = builtins.substring 0 1 url; in
626+ if firstChar == "/" then /. + url
627+ else if firstChar == "." then ./. + url
628+ else url;
629+ in if !fetchSubmodules && private then builtins.fetchGit {
630+ url = realUrl; inherit rev;
631+ ${if branch == null then null else "ref"} = branch;
632+ } else (import <nixpkgs> {}).fetchgit {
633+ url = realUrl; inherit rev sha256;
634+ };
635+ json = builtins.fromJSON (builtins.readFile ./git.json);
636+in fetch json
637+|]
638+
639+parseGitJsonBytes :: LBS.ByteString -> Either String ThunkPtr
640+parseGitJsonBytes = parseJsonObject $ parseThunkPtr $ fmap ThunkSource_Git . parseGitSource
641+
642+mkThunkSpec :: Text -> FilePath -> (LBS.ByteString -> Either String ThunkPtr) -> Text -> ThunkSpec
643+mkThunkSpec name jsonFileName parser srcNix = ThunkSpec name $ Map.fromList
644+ [ ("default.nix", ThunkFileSpec_FileMatches defaultNixViaSrc)
645+ , ("thunk.nix", ThunkFileSpec_FileMatches srcNix)
646+ , (jsonFileName, ThunkFileSpec_Ptr parser)
647+ , (attrCacheFileName, ThunkFileSpec_AttrCache)
648+ , (normalise $ unpackedDirName </> ".git", ThunkFileSpec_CheckoutIndicator)
649+ ]
650+ where
651+ defaultNixViaSrc = [here|
652+# DO NOT HAND-EDIT THIS FILE
653+import (import ./thunk.nix)
654+|]
577655
578−--TODO: when checking something out, make a shallow clone
656+
657+parseJsonObject :: (Aeson.Object -> Aeson.Parser a) -> LBS.ByteString -> Either String a
658+parseJsonObject p bytes = Aeson.parseEither p =<< Aeson.eitherDecode bytes
579659
580660-- | Checks a cache directory to see if there is a fresh symlink
581661-- to the result of building an attribute of a thunk.
@@ -583,44 +663,49 @@ thunkFileNames = ["default.nix", "github.json", "git.json"]
583663-- and the result is symlinked into the cache.
584664nixBuildThunkAttrWithCache
585665 :: MonadObelisk m
586− => FilePath
666+ => ThunkSpec
667+ -> FilePath
587668 -- ^ Path to directory containing Thunk
588669 -> String
589670 -- ^ Attribute to build
590− -> m FilePath
671+ -> m (Maybe FilePath)
591672 -- ^ Symlink to cached or built nix output
592673-- WARNING: If the thunk uses an impure reference such as '<nixpkgs>'
593674-- the caching mechanism will fail as it merely measures the modification
594675-- time of the cache link and the expression to build.
595−nixBuildThunkAttrWithCache thunkDir attr = do
596− --NB: Expects thunkDir to be normalised with no trailing path separator.
597− --This should be guaranteed by the command argument parser.
598− let cacheErrHandler e
599− | isDoesNotExistError e = return Nothing -- expected from a cache miss
600− | otherwise = putLog Error (T.pack $ displayException e) >> return Nothing
601− cacheDir = thunkDir </> attrCacheFileName
602− cachePath = cacheDir </> attr <.> "out"
676+nixBuildThunkAttrWithCache thunkSpec thunkDir attr = do
603677 latestChange <- liftIO $ do
604− createDirectoryIfMissing False cacheDir
605− let getModificationTimeMaybe = fmap rightToMaybe . try @IOError . getModificationTime
606− maximum . catMaybes <$> mapM (getModificationTimeMaybe . (thunkDir </>)) thunkFileNames
607− cacheHit <- handle cacheErrHandler $ do
608− cacheTime <- liftIO $ posixSecondsToUTCTime . realToFrac . modificationTime <$> getSymbolicLinkStatus cachePath
609− return $ if latestChange <= cacheTime
610− then Just cachePath
611− else Nothing
612− case cacheHit of
613− Just c -> return c
614− Nothing -> do
615− putLog Warning $ T.pack $ mconcat [thunkDir, ": ", attr, " not cached, building ..."]
616− _ <- nixCmd $ NixCmd_Build $ def
617− Lens.& nixBuildConfig_outLink Lens..~ OutLink_IndirectRoot cachePath
618− Lens.& nixCmdConfig_target Lens..~ Target
619− { _target_path = Just thunkDir
620− , _target_attr = Just attr
621− , _target_expr = Nothing
622− }
623− return cachePath
678+ let
679+ getModificationTimeMaybe = fmap rightToMaybe . try @IOError . getModificationTime
680+ thunkFileNames = Map.keys $ _thunkSpec_files thunkSpec
681+ maximum . catMaybes <$> traverse (getModificationTimeMaybe . (thunkDir </>)) thunkFileNames
682+
683+ let cachePaths' = nonEmpty $ Map.keys $ Map.filter (\case ThunkFileSpec_AttrCache -> True; _ -> False) $
684+ _thunkSpec_files thunkSpec
685+ for cachePaths' $ \cachePaths ->
686+ fmap NonEmpty.head $ for cachePaths $ \cacheDir -> do
687+ let
688+ cachePath = thunkDir </> cacheDir </> attr <.> "out"
689+ cacheErrHandler e
690+ | isDoesNotExistError e = pure Nothing -- expected from a cache miss
691+ | otherwise = Nothing <$ putLog Error (T.pack $ displayException e)
692+ cacheHit <- handle cacheErrHandler $ do
693+ cacheTime <- liftIO $ posixSecondsToUTCTime . realToFrac . modificationTime <$> getSymbolicLinkStatus cachePath
694+ pure $ if latestChange <= cacheTime
695+ then Just cachePath
696+ else Nothing
697+ case cacheHit of
698+ Just c -> pure c
699+ Nothing -> do
700+ putLog Warning $ T.pack $ mconcat [thunkDir, ": ", attr, " not cached, building ..."]
701+ liftIO $ createDirectoryIfMissing True (takeDirectory cachePath)
702+ (cachePath <$) $ nixCmd $ NixCmd_Build $ def
703+ & nixBuildConfig_outLink .~ OutLink_IndirectRoot cachePath
704+ & nixCmdConfig_target .~ Target
705+ { _target_path = Just thunkDir
706+ , _target_attr = Just attr
707+ , _target_expr = Nothing
708+ }
624709
625710-- | Build a nix attribute, and cache the result if possible
626711nixBuildAttrWithCache
@@ -631,13 +716,15 @@ nixBuildAttrWithCache
631716 -- ^ Attribute to build
632717 -> m FilePath
633718 -- ^ Symlink to cached or built nix output
634−nixBuildAttrWithCache exprPath attr = do
635− readThunk exprPath >>= \case
636− -- Only packed thunks are cached. in particular, checkouts are not
637− Right (ThunkData_Packed _) -> nixBuildThunkAttrWithCache exprPath attr
638− _ -> nixCmd $ NixCmd_Build $ def
639− Lens.& nixBuildConfig_outLink Lens..~ OutLink_None
640− Lens.& nixCmdConfig_target Lens..~ Target
719+nixBuildAttrWithCache exprPath attr = readThunk exprPath >>= \case
720+ -- Only packed thunks are cached. In particular, checkouts are not.
721+ Right (ThunkData_Packed spec _) ->
722+ maybe build pure =<< nixBuildThunkAttrWithCache spec exprPath attr
723+ _ -> build
724+ where
725+ build = nixCmd $ NixCmd_Build $ def
726+ & nixBuildConfig_outLink .~ OutLink_None
727+ & nixCmdConfig_target .~ Target
641728 { _target_path = Just exprPath
642729 , _target_attr = Just attr
643730 , _target_expr = Nothing
@@ -658,7 +745,7 @@ updateThunk p f = withSystemTempDirectory "obelisk-thunkptr-" $ \tmpDir -> do
658745 where
659746 copyThunkToTmp tmpDir thunkDir = readThunk thunkDir >>= \case
660747 Left err -> failWith $ "withThunkUnpacked: " <> T.pack (show err)
661− Right (ThunkData_Packed _) -> do
748+ Right ThunkData_Packed{} -> do
662749 let tmpThunk = tmpDir </> "thunk"
663750 callProcessAndLogOutput (Notice, Error) $
664751 proc cp ["-r", "-T", thunkDir, tmpThunk]
@@ -672,62 +759,81 @@ updateThunk p f = withSystemTempDirectory "obelisk-thunkptr-" $ \tmpDir -> do
672759finalMsg :: Bool -> (a -> Text) -> Maybe (a -> Text)
673760finalMsg noTrail s = if noTrail then Nothing else Just s
674761
675−unpackThunk :: MonadObelisk m => FilePath -> m ()
676−unpackThunk = unpackThunk' False
677−
678762-- | Check that we are not somewhere inside the thunk directory
679−checkThunkDirectory :: MonadObelisk m => Text -> FilePath -> m ()
680−checkThunkDirectory msg thunkDir = do
763+checkThunkDirectory :: MonadObelisk m => FilePath -> m ()
764+checkThunkDirectory thunkDir = do
681765 currentDir <- liftIO getCurrentDirectory
682766 thunkDir' <- liftIO $ canonicalizePath thunkDir
683− when (thunkDir' `L.isInfixOf` currentDir) $ failWith msg
767+ when (thunkDir' `L.isInfixOf` currentDir) $
768+ failWith [i|Can't perform thunk operations from within the thunk directory: ${thunkDir}|]
769+
770+ -- Don't let thunk commands work when directly given an unpacked repo
771+ when (takeFileName thunkDir == unpackedDirName) $
772+ readThunk (takeDirectory thunkDir) >>= \case
773+ Right _ -> failWith [i|Refusing to perform thunk operation on ${thunkDir} because it is a thunk's unpacked source|]
774+ Left _ -> pure ()
775+
776+unpackThunk :: MonadObelisk m => FilePath -> m ()
777+unpackThunk = unpackThunk' False
684778
685779unpackThunk' :: MonadObelisk m => Bool -> FilePath -> m ()
686−unpackThunk' noTrail thunkDir = checkThunkDirectory "Can't pack/unpack from within the thunk directory" thunkDir >> readThunk thunkDir >>= \case
687− Left err -> failWith $ "thunk unpack: " <> T.pack (show err)
780+unpackThunk' noTrail thunkDir = checkThunkDirectory thunkDir *> readThunk thunkDir >>= \case
781+ Left err -> failWith [i|Invalid thunk at ${thunkDir}: ${err}|]
688782 --TODO: Overwrite option that rechecks out thunk; force option to do so even if working directory is dirty
689− Right (ThunkData_Checkout _) -> failWith "thunk unpack: thunk is already unpacked"
690− Right (ThunkData_Packed tptr) -> do
783+ Right ThunkData_Checkout -> failWith [i|Thunk at ${thunkDir} is already unpacked|]
784+ Right (ThunkData_Packed _ tptr) -> do
691785 let (thunkParent, thunkName) = splitFileName thunkDir
692− withTempDirectory thunkParent thunkName $ \tmpRepo -> do
693− let obGitDir = tmpRepo </> ".git" </> "obelisk"
694− s = case _thunkPtr_source tptr of
695− ThunkSource_GitHub s' -> forgetGithub False s'
696− ThunkSource_Git s' -> s'
786+ withTempDirectory thunkParent thunkName $ \tmpThunk -> do
787+ let
788+ gitSrc = thunkSourceToGitSource $ _thunkPtr_source tptr
789+ newSpec = case _thunkPtr_source tptr of
790+ ThunkSource_GitHub _ -> NonEmpty.head gitHubThunkSpecs
791+ ThunkSource_Git _ -> NonEmpty.head gitThunkSpecs
697792 withSpinner' ("Fetching thunk " <> T.pack thunkName)
698793 (finalMsg noTrail $ const $ "Fetched thunk " <> T.pack thunkName) $ do
699− let git = callProcessAndLogOutput (Notice, Notice) . gitProc tmpRepo
700− git $ [ "clone" ]
701− ++ ("--recursive" <$ guard (_gitSource_fetchSubmodules s))
702− ++ [ T.unpack $ gitUriToText $ _gitSource_url s ]
703− ++ do branch <- maybeToList $ _gitSource_branch s
704− [ "--branch", T.unpack $ untagName branch ]
705− git ["reset", "--hard", Ref.toHexString $ _thunkRev_commit $ _thunkPtr_rev tptr]
706− when (_gitSource_fetchSubmodules s) $
707− git ["submodule", "update", "--recursive", "--init"]
708−
709− liftIO $ createDirectory obGitDir
710− callProcessAndLogOutput (Notice, Error) $
711− proc cp ["-r", "-T", thunkDir </> ".", obGitDir </> "orig-thunk"]
712− callProcessAndLogOutput (Notice, Error) $
713− proc "rm" ["-r", thunkDir]
714− callProcessAndLogOutput (Notice, Error) $
715− proc "mv" ["-T", tmpRepo, thunkDir]
794+ let unpackedPath = tmpThunk </> unpackedDirName
795+ gitCloneForThunkUnpack gitSrc (_thunkRev_commit $ _thunkPtr_rev tptr) unpackedPath
796+
797+ let normalizeMore = dropTrailingPathSeparator . normalise
798+ when (normalizeMore unpackedPath /= normalizeMore tmpThunk) $ -- Only write meta data if the checkout is not inplace
799+ createThunk tmpThunk $ Left newSpec
800+
801+ liftIO $ do
802+ removePathForcibly thunkDir
803+ renameDirectory tmpThunk thunkDir
804+
805+gitCloneForThunkUnpack
806+ :: MonadObelisk m
807+ => GitSource -- ^ Git source to use
808+ -> Ref hash -- ^ Commit hash to reset to
809+ -> FilePath -- ^ Directory to clone into
810+ -> m ()
811+gitCloneForThunkUnpack gitSrc commit dir = do
812+ let git = callProcessAndLogOutput (Notice, Notice) . gitProc dir
813+ git $ [ "clone" ]
814+ ++ ["--recursive" | _gitSource_fetchSubmodules gitSrc]
815+ ++ [ T.unpack $ gitUriToText $ _gitSource_url gitSrc ]
816+ ++ do branch <- maybeToList $ _gitSource_branch gitSrc
817+ [ "--branch", T.unpack $ untagName branch ]
818+ git ["reset", "--hard", Ref.toHexString commit]
819+ when (_gitSource_fetchSubmodules gitSrc) $
820+ git ["submodule", "update", "--recursive", "--init"]
716821
717822--TODO: add a rollback mode to pack to the original thunk
718823packThunk :: MonadObelisk m => ThunkPackConfig -> FilePath -> m ThunkPtr
719824packThunk = packThunk' False
720825
721826packThunk' :: MonadObelisk m => Bool -> ThunkPackConfig -> FilePath -> m ThunkPtr
722−packThunk' noTrail (ThunkPackConfig force thunkConfig) thunkDir = checkThunkDirectory "Can't pack/unpack from within the thunk directory" thunkDir >> readThunk thunkDir >>= \case
723− Left err -> failWith $ T.pack $ "thunk pack: " <> show err
724− Right (ThunkData_Packed _) -> failWith "pack: thunk is already packed"
725− Right (ThunkData_Checkout _) -> do
726− withSpinner' ("Packing thunk " <> T.pack thunkDir)
727− (finalMsg noTrail $ const $ "Packed thunk " <> T.pack thunkDir) $ do
728− thunkPtr <- modifyThunkPtrByConfig thunkConfig <$> getThunkPtr (not force) thunkDir (_thunkConfig_private thunkConfig)
729− callProcessAndLogOutput (Debug, Error) $ proc "rm" ["-rf", thunkDir]
730− liftIO $ createThunk thunkDir thunkPtr
827+packThunk' noTrail (ThunkPackConfig force thunkConfig) thunkDir = checkThunkDirectory thunkDir *> readThunk thunkDir >>= \case
828+ Right ThunkData_Packed{} -> failWith [i|Thunk at ${thunkDir} is is already packed|]
829+ _ -> withSpinner'
830+ ("Packing thunk " <> T.pack thunkDir)
831+ (finalMsg noTrail $ const $ "Packed thunk " <> T.pack thunkDir) $
832+ do
833+ let checkClean = if force then CheckClean_NoCheck else CheckClean_FullCheck
834+ thunkPtr <- modifyThunkPtrByConfig thunkConfig <$> getThunkPtr checkClean thunkDir (_thunkConfig_private thunkConfig)
835+ liftIO $ removePathForcibly thunkDir
836+ createThunk thunkDir $ Right thunkPtr
731837 pure thunkPtr
732838
733839modifyThunkPtrByConfig :: ThunkConfig -> ThunkPtr -> ThunkPtr
@@ -738,29 +844,43 @@ modifyThunkPtrByConfig (ThunkConfig markPrivate') ptr = case markPrivate' of
738844 ThunkSource_GitHub s -> ThunkSource_GitHub $ s { _gitHubSource_private = markPrivate }
739845 }
740846
741−getThunkPtr :: forall m. MonadObelisk m => Bool -> FilePath -> Maybe Bool -> m ThunkPtr
742−getThunkPtr checkClean thunkDir mPrivate = do
743− when checkClean $ ensureCleanGitRepo thunkDir True
847+data CheckClean
848+ = CheckClean_FullCheck
849+ -- ^ Check that the repo is clean, including .gitignored files
850+ | CheckClean_NotIgnored
851+ -- ^ Check that the repo is clean, not including .gitignored files
852+ | CheckClean_NoCheck
853+ -- ^ Don't check that the repo is clean
854+
855+getThunkPtr :: forall m. MonadObelisk m => CheckClean -> FilePath -> Maybe Bool -> m ThunkPtr
856+getThunkPtr gitCheckClean dir mPrivate = do
857+ let repoLocations = nubOrd $ map (first normalise)
858+ [(".git", "."), (unpackedDirName </> ".git", unpackedDirName)]
859+ repoLocation' <- liftIO $ flip findM repoLocations $ doesDirectoryExist . (dir </>) . fst
860+ thunkDir <- case repoLocation' of
861+ Nothing -> failWith [i|Can't find an unpacked thunk in ${dir}|]
862+ Just (_, path) -> pure $ normalise $ dir </> path
863+
864+ let (checkClean, checkIgnored) = case gitCheckClean of
865+ CheckClean_FullCheck -> (True, True)
866+ CheckClean_NotIgnored -> (True, False)
867+ CheckClean_NoCheck -> (False, False)
868+ when checkClean $ ensureCleanGitRepo thunkDir checkIgnored
744869 "thunk pack: thunk checkout contains unsaved modifications"
745870
746871 -- Check whether there are any stashes
747− stashOutput <- readGitProcess thunkDir ["stash", "list"]
748− when checkClean $ case T.null stashOutput of
749− False -> do
872+ when checkClean $ do
873+ stashOutput <- readGitProcess thunkDir ["stash", "list"]
874+ unless (T.null stashOutput) $
750875 failWith $ T.unlines $
751876 [ "thunk pack: thunk checkout has stashes"
752877 , "git stash list:"
753878 ] ++ T.lines stashOutput
754− True -> return ()
755879
756880 -- Get current branch
757881 (mCurrentBranch, mCurrentCommit) <- do
758− b <- listToMaybe
759− <$> T.lines
760− <$> readGitProcess thunkDir ["rev-parse", "--abbrev-ref", "HEAD"]
761− c <- listToMaybe
762− <$> T.lines
763− <$> readGitProcess thunkDir ["rev-parse", "HEAD"]
882+ b <- listToMaybe . T.lines <$> readGitProcess thunkDir ["rev-parse", "--abbrev-ref", "HEAD"]
883+ c <- listToMaybe . T.lines <$> readGitProcess thunkDir ["rev-parse", "HEAD"]
764884 case b of
765885 (Just "HEAD") -> failWith $ T.unlines
766886 [ "thunk pack: You are in 'detached HEAD' state."
@@ -771,7 +891,7 @@ getThunkPtr checkClean thunkDir mPrivate = do
771891
772892 -- Get information on all branches and their (optional) designated upstream
773893 -- correspondents
774− (headDump :: [Text]) <- T.lines <$> readGitProcess thunkDir
894+ headDump :: [Text] <- T.lines <$> readGitProcess thunkDir
775895 [ "for-each-ref"
776896 , "--format=%(refname:short) %(upstream:short) %(upstream:remotename)"
777897 , "refs/heads/"
@@ -817,7 +937,7 @@ getThunkPtr checkClean thunkDir mPrivate = do
817937 ]
818938
819939 -- loosely by https://stackoverflow.com/questions/7773939/show-git-ahead-and-behind-info-for-all-branches-including-remotes
820− stats <- iforM headUpstream $ \branch (upstream, _remote) -> do
940+ stats <- ifor headUpstream $ \branch (upstream, _remote) -> do
821941 (stat :: [Text]) <- T.lines <$> readGitProcess thunkDir
822942 [ "rev-list", "--left-right"
823943 , T.unpack branch <> "..." <> T.unpack upstream
@@ -841,8 +961,9 @@ getThunkPtr checkClean thunkDir mPrivate = do
841961 \pushed but this repo's remote tracking branches don't know it.)"
842962 ]
843963
844− -- We assume it's safe to pack the thunk at this point
845− putLog Informational "All changes safe in git remotes. OK to pack thunk."
964+ when checkClean $ do
965+ -- We assume it's safe to pack the thunk at this point
966+ putLog Informational "All changes safe in git remotes. OK to pack thunk."
846967
847968 let remote = maybe "origin" snd $ flip Map.lookup headUpstream =<< mCurrentBranch
848969
@@ -860,9 +981,7 @@ getThunkPtr checkClean thunkDir mPrivate = do
860981-- | Get the latest revision available from the given source
861982getLatestRev :: MonadObelisk m => ThunkSource -> m ThunkRev
862983getLatestRev os = do
863− let gitS = case os of
864− ThunkSource_GitHub s -> forgetGithub False s
865− ThunkSource_Git s -> s
984+ let gitS = thunkSourceToGitSource os
866985 (_, commit) <- gitGetCommitBranch (_gitSource_url gitS) (untagName <$> _gitSource_branch gitS)
867986 case os of
868987 ThunkSource_GitHub s -> githubThunkRev s commit
lib/command/src/Obelisk/Command/Upgrade/Hash.hsdeleted−114
@@ -1,114 +0,0 @@
1−{-# LANGUAGE FlexibleContexts #-}
2−{-# LANGUAGE LambdaCase #-}
3−{-# LANGUAGE OverloadedStrings #-}
4−{-# LANGUAGE ScopedTypeVariables #-}
5−{-# LANGUAGE TupleSections #-}
6−-- | Hash a git repo or arbitrary directory using nix-hash.
7−module Obelisk.Command.Upgrade.Hash
8− ( getDirectoryHash
9− , getHashAtGitRevision
10− ) where
11−
12−import Control.Monad (forM, forM_)
13−import Control.Monad.IO.Class (liftIO)
14−import Data.Bool (bool)
15−import Data.Maybe (catMaybes)
16−import Data.Semigroup ((<>))
17−import Data.Text (Text)
18−import qualified Data.Text as T
19−import System.Directory
20−import System.FilePath
21−import System.IO.Temp
22−import System.Process (cwd, proc)
23−
24−import Obelisk.App (MonadObelisk)
25−import Obelisk.CliApp
26−import Obelisk.Command.Utils
27−
28−import Obelisk.Migration
29−
30−-- | Get the unique hash of the given directory
31−--
32−-- Excludes the following before computing the hash:
33−-- * the specified top-level files/ directories.
34−-- * .git directory
35−-- * untracked Git files
36−-- * ignored Git files
37−-- * empty directories
38−--
39−-- Uses the same predictive algorithm as that of Nix (`nix-hash`).
40−--
41−-- This function will do a full copy of the directory to a temporary location before
42−-- computing the hash. Because it will be deleting the files in exclude list, and
43−-- other files if the directory is a git repo, which needs to be done as `nix-hash`
44−-- doesn't support taking an excludes list.
45−getDirectoryHash :: MonadObelisk m => [FilePath] -> FilePath -> m Hash
46−getDirectoryHash excludes dir = withSystemTempDirectory "obelisk-hash-" $ \tmpDir -> do
47− withSpinnerNoTrail (T.pack $ "Copying " <> dir <> " to " <> tmpDir) $ do
48− runProc $ copyDir dir tmpDir
49− getDirectoryHashDestructive excludes tmpDir
50−
51−getDirectoryHashDestructive :: MonadObelisk m => [FilePath] -> FilePath -> m Hash
52−getDirectoryHashDestructive excludes dir = do
53− liftIO (doesDirectoryExist $ dir </> ".git") >>= \case
54− True -> do
55− tidyUpGitWorkingCopy dir
56− withSpinnerNoTrail "Removing .git directory" $
57− liftIO $ removePathForcibly $ dir </> ".git"
58− False -> pure ()
59− withSpinnerNoTrail "Removing excluded paths" $ do
60− forM_ (fmap (dir </>) excludes) $
61− liftIO . removePathForcibly
62− nixHash dir
63−
64−getHashAtGitRevision :: MonadObelisk m => [Text] -> [FilePath] -> FilePath -> m [Hash]
65−getHashAtGitRevision revs excludes dir = withSystemTempDirectory "obelisk-hashrev-" $ \tmpDir -> do
66− withSpinnerNoTrail (T.pack $ "Copying " <> dir <> " to " <> tmpDir) $ do
67− runProc $ copyDir dir tmpDir
68− tidyUpGitWorkingCopy tmpDir
69− -- Discard changes to tracked files
70− runProcSilently $ gitProc tmpDir ["reset", "--hard"]
71− forM revs $ \rev -> do
72− runProcSilently $ gitProc tmpDir ["checkout", T.unpack rev]
73− -- Checking out an arbitrary revision can leave untracked files (from
74− -- previous revison) around, so tidy them up.
75− tidyUpGitWorkingCopy tmpDir
76− withFilesStashed tmpDir (excludes <> [".git"]) $
77− nixHash tmpDir
78− where
79− withFilesStashed base fs m = withSystemTempDirectory "obelisk-hashrev-stash-" $ \stashDir -> do
80− existingPaths <- fmap catMaybes $ forM fs $ \p -> do
81− liftIO (doesPathExist $ base </> p) >>= pure . bool Nothing (Just p)
82− forM_ existingPaths $ \p ->
83− liftIO $ renamePath (base </> p) (stashDir </> p)
84− result <- m
85− forM_ existingPaths $ \p ->
86− liftIO $ renamePath (stashDir </> p) (base </> p)
87− return result
88−
89−nixHash :: MonadObelisk m => FilePath -> m Hash
90−nixHash dir = withSpinnerNoTrail "Running `nix-hash`" $
91− readProc $ proc "nix-hash" [dir]
92−
93−-- | Clean up the following files in the git working copy
94−--
95−-- * Paths ignored by .gitignore, and still present
96−-- * Untracked files (not added to git index)
97−-- * Any empty directories (these are not tracked by git)
98−--
99−-- Note that this leaves modified (staged or unstaged) files as they are.
100−tidyUpGitWorkingCopy :: MonadObelisk m => FilePath -> m ()
101−tidyUpGitWorkingCopy dir = withSpinnerNoTrail "Tidying up git working copy" $ do
102− ignored <- gitLsFiles dir ["--ignored", "--exclude-standard", "--others"]
103− untracked <- gitLsFiles dir ["--exclude-standard", "--others"]
104− putLog Debug $ "Ignored: " <> T.pack (show $ length ignored) <> " files."
105− putLog Debug $ "Untracked files:\n" <> T.unlines untracked
106− withSpinnerNoTrail "Removing untracked and ignored files" $ do
107− forM_ (fmap ((dir </>) . T.unpack) $ ignored <> untracked) $
108− liftIO . removePathForcibly
109− -- Empty directories won't be included in these lists. Git doesn't track them
110− -- So we must delete these separately.
111− runProc $ proc "find" [dir, "-depth", "-empty", "-type", "d", "-delete"]
112− where
113− gitLsFiles pwd opts = fmap T.lines $ readProc $
114− (proc "git" $ ["ls-files", "."] <> opts) { cwd = Just pwd }
lib/command/src/Obelisk/Command/Utils.hs+17−2
@@ -10,19 +10,20 @@ module Obelisk.Command.Utils where
1010import Control.Applicative hiding (many)
1111import Control.Monad.Except
1212import Data.Bool (bool)
13−import qualified Text.Megaparsec.Char.Lexer as ML
1413import Data.Bifunctor
1514import Data.Char
1615import Data.Either
17−import Data.Semigroup ((<>))
16+import Data.List (isInfixOf)
1817import Data.Map (Map)
1918import qualified Data.Map as M
2019import Data.Maybe (maybeToList)
20+import Data.Semigroup ((<>))
2121import Data.Text (Text)
2222import qualified Data.Text as T
2323import Data.Void (Void)
2424import System.Exit (ExitCode)
2525import System.Which (staticWhich)
26+import qualified Text.Megaparsec.Char.Lexer as ML
2627import Text.Megaparsec as MP
2728import Text.Megaparsec.Char as MP
2829
@@ -32,6 +33,12 @@ import Obelisk.CliApp
3233cp :: FilePath
3334cp = $(staticWhich "cp")
3435
36+mvPath :: FilePath
37+mvPath = $(staticWhich "mv")
38+
39+rmPath :: FilePath
40+rmPath = $(staticWhich "rm")
41+
3542ghcidExePath :: FilePath
3643ghcidExePath = $(staticWhich "ghcid")
3744
@@ -47,6 +54,14 @@ nixBuildExePath = $(staticWhich "nix-build")
4754jreKeyToolPath :: FilePath
4855jreKeyToolPath = $(staticWhich "keytool")
4956
57+-- | Nix syntax requires relative paths to be prefixed by @./@ or
58+-- @../@. This will make a 'FilePath' that can be embedded in a Nix
59+-- expression.
60+toNixPath :: FilePath -> FilePath
61+toNixPath root | "/" `isInfixOf` root = root
62+ | otherwise = "./" <> root
63+
64+
5065-- Check whether the working directory is clean
5166checkGitCleanStatus :: MonadObelisk m => FilePath -> Bool -> m Bool
5267checkGitCleanStatus repo withIgnored = do
lib/command/src/Obelisk/Command/VmBuilder.hs+2−1
@@ -21,6 +21,7 @@ import qualified System.Info
2121
2222import Obelisk.App (MonadObelisk, getObeliskUserStateDir)
2323import Obelisk.CliApp
24+import Obelisk.Command.Utils (rmPath)
2425
2526-- | Generate the `--builders` argument string to enable the VM builder after ensuring it is available.
2627getNixBuildersArg :: MonadObelisk m => m String
@@ -95,7 +96,7 @@ setupNixDocker stateDir = withSpinner ("Creating Docker container named " <> con
9596
9697 -- Create new SSH keys for this container
9798 callProcessAndLogOutput (Debug, Error) $
98− proc "rm" ["-f", stateDir </> sshKeyFileName, stateDir </> sshKeyFileName <.> "pub"]
99+ proc rmPath ["-f", stateDir </> sshKeyFileName, stateDir </> sshKeyFileName <.> "pub"]
99100 callProcessAndLogOutput (Debug, Error) $
100101 proc "ssh-keygen" ["-t", "ed25519", "-f", stateDir </> sshKeyFileName, "-P", ""]
101102
lib/executable-config/default.nix+25−18
@@ -2,26 +2,33 @@
22# configuration information into a canonical location. It also provides a haskell
33# package that can be used to retrieve the injected configuration on each supported
44# platform.
5−{ nixpkgs
6−, filterGitSource # TODO define this in obelisk
5+{ lib
6+, runCommand
7+, obeliskCleanSource
78}:
8−let injectConfig = config: assets: nixpkgs.runCommand "inject-config" {} (''
9− set -x
10− mkdir -p $out
11− cp --no-preserve=mode -Lr "${assets}" $out/static
12− chmod +w "$out"
13− '' + nixpkgs.lib.optionalString (!(builtins.isNull config)) ''
14− if ! mkdir $out/config; then
15− 2>&1 echo config directory already exists or could not be created
16− exit 1
17− fi
18− cp -a "${config}"/* "$out/config"
19− find "$out/config" -type f -printf '%P\0' >"$out/config.files"
20− '');
21−in with nixpkgs.haskell.lib; {
9+
10+let
11+ injectConfig = config: assets: runCommand "inject-config" {} (''
12+ set -x
13+ mkdir -p $out
14+ cp --no-preserve=mode -Lr "${assets}" $out/static
15+ chmod +w "$out"
16+ '' + lib.optionalString (!(builtins.isNull config)) ''
17+ if ! mkdir $out/config; then
18+ 2>&1 echo config directory already exists or could not be created
19+ exit 1
20+ fi
21+ cp -a "${config}"/* "$out/config"
22+ # Needed for android deployments
23+ find "$out/config" -type f -printf '%P\0' > "$out/config.files"
24+ '');
25+in
26+
27+{
2228 haskellOverlay = self: super: {
23− obelisk-executable-config-lookup = self.callCabal2nix "obelisk-executable-config-lookup" (filterGitSource ./lookup) {};
29+ obelisk-executable-config-lookup = self.callCabal2nix "obelisk-executable-config-lookup" (obeliskCleanSource ./lookup) {};
2430 };
31+
2532 platforms = {
2633 android = {
2734 # Inject the given config directory into an android assets folder
@@ -32,7 +39,7 @@ in with nixpkgs.haskell.lib; {
3239 inject = injectConfig;
3340 };
3441 web = {
35− inject = self: self.callCabal2nix "obelisk-executable-config-inject" (filterGitSource ./inject) {};
42+ inject = self: self.callCabal2nix "obelisk-executable-config-inject" (obeliskCleanSource ./inject) {};
3643 };
3744 };
3845}
lib/route/src/Obelisk/Route.hs+7−5
@@ -50,6 +50,7 @@ module Obelisk.Route
5050 , pathParamEncoder
5151 , pathLiteralEncoder
5252 , singletonListEncoder
53+ , packTextEncoder
5354 , unpackTextEncoder
5455 , prefixTextEncoder
5556 , unsafeTshowEncoder
@@ -159,6 +160,7 @@ import Data.Set (Set)
159160import qualified Data.Set as Set
160161import Data.Some (Some(Some))
161162import Data.Text (Text)
163+import Data.Text.Lens (IsText, packed, unpacked)
162164import qualified Data.Text as T
163165import qualified Data.Text.Encoding as T
164166import Data.Universe
@@ -756,11 +758,11 @@ prefixNonemptyTextEncoder p = Encoder $ pure $ EncoderImpl
756758 Just stripped -> pure stripped
757759 }
758760
759−unpackTextEncoder :: (Applicative check, Applicative parse) => Encoder check parse Text String
760−unpackTextEncoder = Encoder $ pure $ EncoderImpl
761− { _encoderImpl_encode = T.unpack
762− , _encoderImpl_decode = pure . T.pack
763− }
761+packTextEncoder :: (Applicative check, Applicative parse, IsText text) => Encoder check parse String text
762+packTextEncoder = isoEncoder packed
763+
764+unpackTextEncoder :: (Applicative check, Applicative parse, IsText text) => Encoder check parse text String
765+unpackTextEncoder = isoEncoder unpacked
764766
765767toListMapEncoder :: (Applicative check, Applicative parse, Ord k) => Encoder check parse (Map k v) [(k, v)]
766768toListMapEncoder = Encoder $ pure $ EncoderImpl
lib/selftest/obelisk-selftest.cabal+2−1
@@ -9,6 +9,8 @@ library
99 hs-source-dirs: src
1010 build-depends:
1111 base
12+ , bytestring
13+ , aeson
1214 , containers
1315 , directory
1416 , filepath
@@ -17,7 +19,6 @@ library
1719 , http-types
1820 , HUnit
1921 , network
20− , obelisk-cliapp
2122 , obelisk-executable-config-lookup
2223 , obelisk-run
2324 , which
lib/selftest/src/Obelisk/SelfTest.hs+398−230
@@ -4,39 +4,45 @@
44{-# LANGUAGE OverloadedStrings #-}
55{-# LANGUAGE PatternGuards #-}
66{-# LANGUAGE ScopedTypeVariables #-}
7+{-# LANGUAGE TemplateHaskell #-}
8+{-# LANGUAGE TypeApplications #-}
79{-# LANGUAGE ViewPatterns #-}
8−{-# LANGUAGE TemplateHaskell #-}
910module Obelisk.SelfTest where
1011
11−import Control.Exception (bracket, throw)
12+import Control.Concurrent (threadDelay)
13+import Control.Exception (bracket, throw, try)
1214import Control.Monad
1315import Control.Monad.IO.Class
16+import qualified Data.Aeson as Aeson
17+import qualified Data.Aeson.Types as Aeson
1418import Data.Bool (bool)
19+import qualified Data.ByteString.Lazy as LBS
20+import Data.Foldable (for_)
1521import Data.Function (fix)
22+import qualified Data.Map as Map
1623import Data.Semigroup ((<>))
1724import qualified Data.Set as Set
1825import Data.String
1926import Data.Text (Text)
20−import Data.Void
2127import qualified Data.Text as T
28+import qualified Data.Text.Encoding as T
2229import qualified Data.Text.IO as T
2330import qualified Network.HTTP.Client as HTTP
2431import qualified Network.HTTP.Types as HTTP
2532import qualified Network.Socket as Socket
26−import Shelly hiding (cp)
33+import Shelly
2734import System.Directory (getCurrentDirectory, withCurrentDirectory, getDirectoryContents)
2835import System.Environment
2936import System.Exit (ExitCode (..))
30−import System.Which (staticWhich)
37+import System.FilePath (addTrailingPathSeparator)
3138import qualified System.Info
3239import System.IO (Handle, hClose, hIsEOF, hGetContents)
3340import System.IO.Temp
34−import System.Process (readProcessWithExitCode, CreateProcess(cwd), readCreateProcessWithExitCode, proc)
41+import System.Process (readProcessWithExitCode)
42+import System.Which (staticWhich)
3543import Test.Hspec
3644import Test.HUnit.Base
3745
38−import Obelisk.CliApp hiding (runCli, readCreateProcessWithExitCode)
39−import qualified Obelisk.CliApp as CliApp
4046import Obelisk.ExecutableConfig.Lookup (getConfigs)
4147import Obelisk.Run (getConfigRoute)
4248
@@ -46,8 +52,8 @@ data ObRunState
4652 | ObRunState_BackendStarted
4753 deriving (Eq, Show)
4854
49−cp :: FilePath
50−cp = $(staticWhich "cp")
55+cpPath :: FilePath
56+cpPath = $(staticWhich "cp")
5157
5258cabalPath :: FilePath
5359cabalPath = $(staticWhich "cabal")
@@ -64,15 +70,24 @@ chmodPath = $(staticWhich "chmod")
6470whoamiPath :: FilePath
6571whoamiPath = $(staticWhich "whoami")
6672
73+nixInstantiatePath :: FilePath
74+nixInstantiatePath = $(staticWhich "nix-instantiate")
75+
6776nixBuildPath :: FilePath
6877nixBuildPath = $(staticWhich "nix-build")
6978
79+nixPrefetchGitPath :: FilePath
80+nixPrefetchGitPath = $(staticWhich "nix-prefetch-git")
81+
7082lnPath :: FilePath
7183lnPath = $(staticWhich "ln")
7284
7385rmPath :: FilePath
7486rmPath = $(staticWhich "rm")
7587
88+rsyncPath :: FilePath
89+rsyncPath = $(staticWhich "rsync")
90+
7691gitUserConfig :: [Text]
7792gitUserConfig = ["-c", "user.name=Obelisk Selftest", "-c", "user.email=noreply@example.com"]
7893
@@ -84,12 +99,6 @@ commit msg = void $ run gitPath $ gitUserConfig <> [ "commit"
8499 , msg
85100 ]
86101
87−-- TODO replace Void and stop using loosely typed synchronous exceptions
88−runCli :: MonadIO m => CliT Void IO a -> m a
89−runCli f = liftIO $ do
90− c <- newCliConfig Notice False False (\case {})
91− CliApp.runCli c f
92−
93102tshow :: Show a => a -> Text
94103tshow = T.pack . show
95104
@@ -106,228 +115,266 @@ ob = "ob"
106115augmentWithVerbosity :: (String -> [Text] -> a) -> String -> Bool -> [Text] -> a
107116augmentWithVerbosity runner executable isVerbose args = runner executable $ (if isVerbose then ("-v" :) else id) args
108117
118+isolatedGitShell :: Bool -> FilePath -> (([Text] -> Sh Text) -> Sh a) -> IO a
119+isolatedGitShell isVerbose dir f = shelly $ bool silently verbosely isVerbose $ do
120+ setenv "HOME" "/dev/null"
121+ setenv "GIT_CONFIG_NOSYSTEM" "1"
122+ _ <- git ["init"]
123+ _ <- git ["config", "user.name", "SelfTest"]
124+ _ <- git ["config", "user.email", "self@test"]
125+ f git
126+ where
127+ git args = run gitPath $ ["-C", toTextIgnore dir] <> args
128+
129+-- | Copies a git repo to a new location and "resets" the git history to include
130+-- exactly one commit with all files added. It then restricts writing and reading
131+-- for group and user to make the repo ideal for being a valid git remote for thunks.
132+--
133+-- Using this allows dirty repos to be used as git remotes during the test since 'git clone'ing
134+-- a dirty repo will not include the uncommitted changes.
135+copyForGitRemote :: Bool -> FilePath -> FilePath -> IO ()
136+copyForGitRemote isVerbose origDir copyDir = isolatedGitShell isVerbose copyDir $ \git -> do
137+ run_ rsyncPath
138+ [ "-r", "--no-perms", "--no-owner", "--no-group", "--exclude", ".git"
139+ , toTextIgnore (addTrailingPathSeparator origDir), toTextIgnore copyDir
140+ ]
141+ _ <- git ["add", "--all"]
142+ _ <- git ["commit", "-m", "Copy repo"]
143+ run_ chmodPath ["-R", "u-w,g-rw,o-rw", toTextIgnore copyDir] -- Freeze this state
144+
109145main :: IO ()
110146main = do
111147 -- Note: you can pass hspec arguments as well, eg: `-m <pattern>`
112148 isVerbose <- elem "-v" <$> getArgs
113149 unless isVerbose $
114150 putStrLn "Tests may take longer to run if there are unbuilt derivations: use -v for verbose output"
115− let verbosity = bool silently verbosely isVerbose
116− nixBuild args = run nixBuildPath ("--no-out-link" : args)
117− obeliskImplDirtyReadOnly <- fromString <$> getCurrentDirectory
118− let runOb_ = augmentWithVerbosity run_ ob isVerbose
119− let runOb = augmentWithVerbosity run ob isVerbose
120− let testObRunInDir' = augmentWithVerbosity testObRunInDir ob isVerbose ["run"]
121− let testThunkPack' = augmentWithVerbosity testThunkPack ob isVerbose []
151+
152+ obeliskImplDirtyReadOnly <- getCurrentDirectory
153+ httpManager <- HTTP.newManager HTTP.defaultManagerSettings
154+
155+ withSystemTempDirectory "obelisk-repo-git-remote" $ \copyDir -> do
156+ copyForGitRemote isVerbose obeliskImplDirtyReadOnly copyDir
157+ main' isVerbose httpManager copyDir
158+
159+main' :: Bool -> HTTP.Manager -> FilePath -> IO ()
160+main' isVerbose httpManager obeliskRepoReadOnly = withInitCache $ \initCache -> hspec $ parallel $ do
122161 let
123− withObeliskImplClean f =
124− withSystemTempDirectory "obelisk-impl-clean" $ \(fromString -> obeliskImpl) -> do
125− void . shellyOb verbosity $ chdir obeliskImpl $ do
126− run_ gitPath ["clone", "file://" <> toTextIgnore obeliskImplDirtyReadOnly, toTextIgnore obeliskImpl]
127− f obeliskImpl
162+ inTmpObInit' dirname f = inTmp' dirname $ \dir -> do
163+ run_ cpPath ["-rT", fromString initCache, toTextIgnore dir]
164+ f dir
165+ inTmpObInit = inTmpObInit' defaultTmpDirName
166+
167+ -- To be used in tests that change the obelisk impl directory
168+ inTmpObInitWithImplCopy f = inTmpObInit $ \dir ->
169+ withObeliskImplClean $ \(fromString -> implClean) -> do
170+ run_ rmPath [thunk]
171+ run_ lnPath ["-s", implClean, thunk]
172+ f dir
173+
174+ describe "ob init" $ parallel $ do
175+ it "works with default impl" $ inTmp $ \_ -> runOb ["init"]
176+ it "works with master branch impl" $ inTmp $ \_ -> runOb ["init", "--branch", "master"]
177+ it "works with symlink" $ inTmp $ \_ -> runOb ["init", "--symlink", toTextIgnore obeliskRepoReadOnly]
178+ it "doesn't silently overwrite existing files" $ inTmp $ \_ -> do
179+ let p force = errExit False $ do
180+ run_ ob $ "--no-handoff" : "-v" : "init" : ["--force"|force]
181+ (== 0) <$> lastExitCode
182+
183+ True <- p False
184+ False <- p False
185+ True <- p True
186+ pure ()
187+
188+ it "doesn't create anything when given an invalid impl" $ inTmp $ \tmp -> do
189+ void $ errExit False $ runOb ["init", "--symlink", "/dev/null"]
190+ ls tmp >>= liftIO . assertEqual "" []
191+
192+ it "produces a valid route config" $ inTmpObInit $ \tmp -> liftIO $
193+ withCurrentDirectory (T.unpack $ toTextIgnore tmp) $ do
194+ configs <- getConfigs
195+ return (either (const Nothing) Just $ getConfigRoute configs) `shouldNotReturn` Nothing
196+
197+ it "can unpack and repack .obelisk/impl after init with master branch impl" $ inTmp $ \_ -> do
198+ runOb_ ["init", "--branch", "master"]
199+ runOb_ ["thunk", "unpack", ".obelisk/impl"]
200+ runOb_ ["thunk", "pack", ".obelisk/impl"]
201+
202+ -- These tests fail with "Could not find module 'Obelisk.Generated.Static'"
203+ -- when not run by 'nix-build --attr selftest'
204+ describe "ob run" $ {- NOT parallel $ -} do
205+ it "works in root directory" $ inTmpObInit $ \_ -> testObRunInDir' Nothing httpManager
206+ it "works in sub directory" $ inTmpObInit $ \_ -> testObRunInDir' (Just "frontend") httpManager
207+
208+ describe "obelisk project" $ parallel $ do
209+ it "can build obelisk command" $ inTmpObInit $ \_ -> nixBuild ["-A", "command" , toTextIgnore obeliskRepoReadOnly]
210+ it "can build obelisk skeleton" $ inTmpObInit $ \_ -> nixBuild ["-A", "skeleton", toTextIgnore obeliskRepoReadOnly]
211+ it "can build obelisk shell" $ inTmpObInit $ \_ -> nixBuild ["-A", "shell", toTextIgnore obeliskRepoReadOnly]
212+ it "can build everything" $ inTmpObInit $ \_ -> nixBuild [toTextIgnore obeliskRepoReadOnly]
213+
214+ describe "blank initialized project" $ parallel $ do
215+
216+ it "can build ghc.backend" $ inTmpObInit $ \_ -> nixBuild ["-A", "ghc.backend"]
217+ it "can build ghcjs.frontend" $ inTmpObInit $ \_ -> nixBuild ["-A", "ghcjs.frontend"]
218+
219+ if System.Info.os == "darwin"
220+ then it "can build ios" $ inTmpObInit $ \_ -> nixBuild ["-A", "ios.frontend"]
221+ else it "can build android after accepting license" $ inTmpObInit $ \dir -> do
222+ let defaultNixPath = dir </> ("default.nix" :: FilePath)
223+ writefile defaultNixPath
224+ =<< T.replace
225+ "# config.android_sdk.accept_license = false;"
226+ "config.android_sdk.accept_license = true;"
227+ <$> readfile defaultNixPath
228+ nixBuild ["-A", "android.frontend"]
229+
230+ forM_ ["ghc", "ghcjs"] $ \compiler -> do
231+ let
232+ shellName = "shells." <> compiler
233+ inShell cmd' = run_ "nix-shell" ["default.nix", "-A", fromString shellName, "--run", cmd']
234+ it ("can enter " <> shellName) $ inTmpObInit $ \_ -> inShell "exit"
235+ -- NOTE: We override the temporary directory name because cabal has a bug preventing new-build from working
236+ -- in a path that has unicode characters.
237+ it ("can build in " <> shellName) $ inTmpObInit' "test" $ \_ -> inShell $
238+ T.pack cabalPath <> " --version; " <> T.pack cabalPath <> " new-build --" <> T.pack compiler <> " all"
239+
240+ it "has idempotent thunk update" $ inTmpObInitWithImplCopy $ \_ -> do
241+ _ <- pack
242+ u <- update
243+ uu <- update
244+ assertRevEQ u uu
245+
246+ describe "ob thunk pack/unpack" $ parallel $ do
247+ it "has thunk pack and unpack inverses" $ inTmpObInitWithImplCopy $ \_ -> do
248+
249+ _ <- pack
250+ e <- commitAll
251+ eu <- unpack
252+ eup <- pack
253+ eupu <- unpack
254+ _ <- pack
255+
256+ assertRevEQ e eup
257+ assertRevEQ eu eupu
258+ assertRevNE e eu
259+
260+ it "unpacks the correct branch" $ withTmp $ \dir -> do
261+ let branch = "master"
262+ run_ gitPath ["clone", "https://github.com/reflex-frp/reflex.git", toTextIgnore dir, "--branch", branch]
263+ runOb_ ["thunk", "pack", toTextIgnore dir]
264+ runOb_ ["thunk", "unpack", toTextIgnore dir]
265+ branch' <- run gitPath ["-C", toTextIgnore $ dir </> unpackedDirName, "rev-parse", "--abbrev-ref", "HEAD"]
266+ liftIO $ assertEqual "" branch (T.strip branch')
267+
268+ it "can pack and unpack plain git repos" $
269+ shelly_ $ withSystemTempDirectory "git-repo" $ \dir -> do
270+ let repo = toTextIgnore $ dir </> ("repo" :: FilePath)
271+ run_ gitPath ["clone", "https://github.com/haskell/process.git", repo]
272+ origHash <- chdir (fromText repo) revParseHead
273+
274+ runOb_ ["thunk", "pack", repo]
275+ packedFiles <- Set.fromList <$> ls (fromText repo)
276+ liftIO $ assertEqual "" packedFiles $ Set.fromList $ (repo </>) <$>
277+ ["default.nix", "thunk.nix", "github.json" :: FilePath]
278+
279+ runOb_ ["thunk", "unpack", repo]
280+ chdir (fromText repo </> unpackedDirName) $ do
281+ unpackHash <- revParseHead
282+ assertRevEQ origHash unpackHash
283+
284+ testThunkPack' $ fromText repo
285+
286+ it "aborts thunk pack when there are uncommitted files" $ inTmpObInitWithImplCopy $ \dir -> do
287+ testThunkPack' (dir </> thunk)
288+
289+ it "works on legacy git thunks" $ testLegacyGitThunks isVerbose
290+
291+ describe "ob thunk update --branch" $ parallel $ do
292+ it "can change a thunk to the latest version of a desired branch" $ withTmp $ \dir -> do
293+ let branch1 = "master"
294+ branch2 = "develop"
295+ run_ gitPath ["clone", "https://github.com/reflex-frp/reflex.git", toTextIgnore dir, "--branch", branch1]
296+ runOb_ ["thunk" , "pack", toTextIgnore dir]
297+ runOb_ ["thunk", "update", toTextIgnore dir, "--branch", branch2]
298+
299+ it "doesn't create anything when given an invalid branch" $ withTmp $ \dir -> do
300+ let checkDir dir' = liftIO $ getDirectoryContents $ T.unpack $ toTextIgnore dir'
301+ run_ gitPath ["clone", "https://github.com/reflex-frp/reflex.git", toTextIgnore dir, "--branch", "master"]
302+ runOb_ ["thunk" , "pack", toTextIgnore dir]
303+ startingContents <- checkDir dir
304+ void $ errExit False $ runOb ["thunk", "update", toTextIgnore dir, "--branch", "dumble-palooza"]
305+ checkDir dir >>= liftIO . assertEqual "" startingContents
306+
307+ describe "ob hoogle" $ {- NOT parallel -} do
308+ it "starts a hoogle server on the given port" $ inTmpObInit $ \_ -> do
309+ [p0] <- liftIO $ getFreePorts 1
310+ maskExitSuccess $ runHandle "ob" ["hoogle", "--port", T.pack (show p0)] $ \stdout -> flip fix Nothing $ \loop -> \case
311+ Nothing -> do -- Still waiting for initial signal that the server has started
312+ ln <- liftIO $ T.hGetLine stdout
313+ let search = "Server starting on port " <> T.pack (show p0)
314+ case search `T.isInfixOf` ln of
315+ False -> loop Nothing -- keep waiting
316+ True -> loop $ Just 10
317+ Just (n :: Int) -> do -- Server has started and we have n attempts left
318+ let req uri = liftIO $ try @HTTP.HttpException $ HTTP.parseRequest uri >>= flip HTTP.httpLbs httpManager
319+ req ("http://127.0.0.1:" <> show p0) >>= \case
320+ Right r | HTTP.responseStatus r == HTTP.ok200 -> exit 0
321+ e -> if n <= 0
322+ then errorExit $ "Request to hoogle server failed: " <> T.pack (show e)
323+ else liftIO (threadDelay (1*10^(6 :: Int))) *> loop (Just $ n - 1)
324+ where
325+ verbosity = bool silently verbosely isVerbose
326+ nixBuild args = run nixBuildPath ("--no-out-link" : args)
128327
129− withObeliskImplDirty f =
130− withSystemTempDirectory "obelisk-impl-copy" $ \(fromString -> obeliskImpl) -> do
131− void . shellyOb verbosity $ chdir obeliskImpl $ do
132− user <- T.strip <$> run whoamiPath []
133− run_ cp ["-rT", "--no-preserve=mode", toTextIgnore obeliskImplDirtyReadOnly, toTextIgnore obeliskImpl]
134− run_ chownPath ["-R", user, toTextIgnore obeliskImpl]
135− run_ chmodPath ["-R", "g-rw,o-rw", toTextIgnore obeliskImpl]
328+ runOb_ = augmentWithVerbosity run_ ob isVerbose
329+ runOb = augmentWithVerbosity run ob isVerbose
330+ testObRunInDir' = augmentWithVerbosity testObRunInDir ob isVerbose ["run"]
331+ testThunkPack' = augmentWithVerbosity testThunkPack ob isVerbose []
332+
333+ withObeliskImplClean f =
334+ withSystemTempDirectory "obelisk-impl-clean" $ \obeliskImpl -> do
335+ void . shellyOb verbosity $ do
336+ dirtyFiles <- T.strip <$> run gitPath ["-C", toTextIgnore obeliskRepoReadOnly, "diff", "--stat"]
337+ () <- when (dirtyFiles /= "") $ error "SelfTest does not work correctly with dirty obelisk repos as remote"
338+ run_ gitPath ["clone", "file://" <> toTextIgnore obeliskRepoReadOnly, toTextIgnore obeliskImpl]
136339 f obeliskImpl
137340
138− withInitCache f obeliskImpl =
341+ withInitCache f =
139342 withSystemTempDirectory "init Cache λ" $ \initCache -> do
140343 -- Setup the ob init cache
141344 void . shellyOb verbosity $ chdir initCache $ do
142− runOb_ ["init", "--symlink", toTextIgnore obeliskImpl]
345+ runOb_ ["init", "--symlink", toTextIgnore obeliskRepoReadOnly]
143346 run_ gitPath ["init"]
144347
145348 f initCache
146349
147− withObeliskImplDirtyAndInitCache f =
148− withObeliskImplDirty $ \impl -> withInitCache (f impl) impl
350+ shelly_ = void . shellyOb verbosity
149351
150− httpManager <- HTTP.newManager HTTP.defaultManagerSettings
352+ defaultTmpDirName = "test λ"
353+
354+ inTmp' :: FilePath -> (FilePath -> Sh a) -> IO ()
355+ inTmp' dirname f = withTmp' dirname (chdir <*> f)
356+ inTmp = inTmp' defaultTmpDirName
357+
358+ withTmp' dirname f = shelly_ . withSystemTempDirectory dirname $ f . fromString
359+ withTmp = withTmp' defaultTmpDirName
360+
361+ assertRevEQ a b = liftIO . assertEqual "" "" =<< diff a b
362+ assertRevNE a b = liftIO . assertBool "" . (/= "") =<< diff a b
363+
364+ revParseHead = T.strip <$> run gitPath ["rev-parse", "HEAD"]
365+
366+ commitAll = do
367+ run_ gitPath ["add", "."]
368+ commit "checkpoint"
369+ revParseHead
370+
371+ thunk = ".obelisk/impl"
372+ update = runOb ["thunk", "update", thunk] *> commitAll
373+ pack = runOb ["thunk", "pack", thunk] *> commitAll
374+ unpack = runOb ["thunk", "unpack", thunk] *> commitAll
375+
376+ diff a b = run gitPath ["diff", a, b]
151377
152− withObeliskImplDirtyAndInitCache $ \obeliskImplDirty initCache ->
153− hspec $ parallel $ do
154− let shelly_ = void . shellyOb verbosity
155−
156− defaultTmpDirName = "test λ"
157−
158− inTmp' :: FilePath -> (FilePath -> Sh a) -> IO ()
159− inTmp' dirname f = withTmp' dirname (chdir <*> f)
160− inTmp = inTmp' defaultTmpDirName
161−
162− withTmp' dirname f = shelly_ . withSystemTempDirectory dirname $ f . fromString
163− withTmp = withTmp' defaultTmpDirName
164−
165− inTmpObInit' dirname f = inTmp' dirname $ \dir -> do
166− run_ cp ["-rT", fromString initCache, toTextIgnore dir]
167− f dir
168− inTmpObInit = inTmpObInit' defaultTmpDirName
169−
170− -- To be used in tests that change the obelisk impl directory
171− inTmpObInitWithImplCopy f = inTmpObInit $ \dir ->
172− withObeliskImplClean $ \(fromString -> implClean) -> do
173− run_ rmPath [thunk]
174− run_ lnPath ["-s", implClean, thunk]
175− f dir
176−
177− assertRevEQ a b = liftIO . assertEqual "" "" =<< diff a b
178− assertRevNE a b = liftIO . assertBool "" . (/= "") =<< diff a b
179−
180− revParseHead = T.strip <$> run gitPath ["rev-parse", "HEAD"]
181−
182− commitAll = do
183− run_ gitPath ["add", "."]
184− commit "checkpoint"
185− revParseHead
186−
187− thunk = ".obelisk/impl"
188− update = runOb ["thunk", "update", thunk] >> commitAll
189− pack = runOb ["thunk", "pack", thunk] >> commitAll
190− unpack = runOb ["thunk", "unpack", thunk] >> commitAll
191−
192− diff a b = run gitPath ["diff", a, b]
193−
194− describe "ob init" $ parallel $ do
195− it "works with default impl" $ inTmp $ \_ -> runOb ["init"]
196− it "works with master branch impl" $ inTmp $ \_ -> runOb ["init", "--branch", "master"]
197− it "works with symlink" $ inTmp $ \_ -> runOb ["init", "--symlink", toTextIgnore obeliskImplDirty]
198− it "doesn't silently overwrite existing files" $ withSystemTempDirectory "ob-init λ" $ \dir -> do
199− let p force = (System.Process.proc ob $ "--no-handoff" : "-v" : "init" : ["--force"|force]) { cwd = Just dir }
200− (ExitSuccess, _, _) <- readCreateProcessWithExitCode (p False) ""
201− (ExitFailure _, _, _) <- readCreateProcessWithExitCode (p False) ""
202− (ExitSuccess, _, _) <- readCreateProcessWithExitCode (p True) ""
203− pure ()
204−
205− it "doesn't create anything when given an invalid impl" $ inTmp $ \tmp -> do
206− void $ errExit False $ runOb ["init", "--symlink", "/dev/null"]
207− ls tmp >>= liftIO . assertEqual "" []
208−
209− it "produces a valid route config" $ inTmpObInit $ \tmp -> liftIO $
210− withCurrentDirectory (T.unpack $ toTextIgnore tmp) $ do
211− configs <- getConfigs
212− return (either (const Nothing) Just $ getConfigRoute configs) `shouldNotReturn` Nothing
213−
214− -- These tests fail with "Could not find module 'Obelisk.Generated.Static'"
215− -- when not run by 'nix-build --attr selftest'
216− describe "ob run" $ {- NOT parallel $ -} do
217− it "works in root directory" $ inTmpObInit $ \_ -> testObRunInDir' Nothing httpManager
218− it "works in sub directory" $ inTmpObInit $ \_ -> testObRunInDir' (Just "frontend") httpManager
219−
220− describe "obelisk project" $ parallel $ do
221− it "can build obelisk command" $ inTmpObInit $ \_ -> nixBuild ["-A", "command" , toTextIgnore obeliskImplDirty]
222− it "can build obelisk skeleton" $ inTmpObInit $ \_ -> nixBuild ["-A", "skeleton", toTextIgnore obeliskImplDirty]
223− it "can build obelisk shell" $ inTmpObInit $ \_ -> nixBuild ["-A", "shell", toTextIgnore obeliskImplDirty]
224− it "can build everything" $ inTmpObInit $ \_ -> nixBuild [toTextIgnore obeliskImplDirty]
225−
226− describe "blank initialized project" $ parallel $ do
227−
228− it "can build ghc.backend" $ inTmpObInit $ \_ -> nixBuild ["-A", "ghc.backend"]
229− it "can build ghcjs.frontend" $ inTmpObInit $ \_ -> nixBuild ["-A", "ghcjs.frontend"]
230−
231− if System.Info.os == "darwin"
232− then it "can build ios" $ inTmpObInit $ \_ -> nixBuild ["-A", "ios.frontend"]
233− else it "can build android after accepting license" $ inTmpObInit $ \dir -> do
234− let defaultNixPath = dir </> ("default.nix" :: FilePath)
235− writefile defaultNixPath
236− =<< T.replace
237− "# config.android_sdk.accept_license = false;"
238− "config.android_sdk.accept_license = true;"
239− <$> readfile defaultNixPath
240− nixBuild ["-A", "android.frontend"]
241−
242− forM_ ["ghc", "ghcjs"] $ \compiler -> do
243− let
244− shellName = "shells." <> compiler
245− inShell cmd' = run_ "nix-shell" ["default.nix", "-A", fromString shellName, "--run", cmd']
246− it ("can enter " <> shellName) $ inTmpObInit $ \_ -> inShell "exit"
247− -- NOTE: We override the temporary directory name because cabal has a bug preventing new-build from working
248− -- in a path that has unicode characters.
249− it ("can build in " <> shellName) $ inTmpObInit' "test" $ \_ -> inShell $
250− T.pack cabalPath <> " --version; " <> T.pack cabalPath <> " new-build --" <> T.pack compiler <> " all"
251−
252− it "has idempotent thunk update" $ inTmpObInitWithImplCopy $ \_ -> do
253− _ <- pack
254− u <- update
255− uu <- update
256− assertRevEQ u uu
257−
258− describe "ob thunk pack/unpack" $ parallel $ do
259− it "has thunk pack and unpack inverses" $ inTmpObInitWithImplCopy $ \_ -> do
260−
261− _ <- pack
262− e <- commitAll
263− eu <- unpack
264− eup <- pack
265− eupu <- unpack
266− _ <- pack
267−
268− assertRevEQ e eup
269− assertRevEQ eu eupu
270− assertRevNE e eu
271−
272− it "unpacks the correct branch" $ withTmp $ \dir -> do
273− let branch = "master"
274− run_ gitPath ["clone", "https://github.com/reflex-frp/reflex.git", toTextIgnore dir, "--branch", branch]
275− runOb_ ["thunk", "pack", toTextIgnore dir]
276− runOb_ ["thunk", "unpack", toTextIgnore dir]
277− branch' <- chdir dir $ run gitPath ["rev-parse", "--abbrev-ref", "HEAD"]
278− liftIO $ assertEqual "" branch (T.strip branch')
279−
280− it "can pack and unpack plain git repos" $
281− shelly_ $ withSystemTempDirectory "git-repo" $ \dir -> do
282− let repo = toTextIgnore $ dir </> ("repo" :: String)
283− run_ gitPath ["clone", "https://github.com/haskell/process.git", repo]
284− origHash <- chdir (fromText repo) revParseHead
285−
286− runOb_ ["thunk", "pack", repo]
287− packedFiles <- Set.fromList <$> ls (fromText repo)
288− liftIO $ assertEqual "" packedFiles $ Set.fromList $ (repo </>) <$>
289− ["default.nix", "github.json", ".attr-cache" :: String]
290−
291− runOb_ ["thunk", "unpack", repo]
292− chdir (fromText repo) $ do
293− unpackHash <- revParseHead
294− assertRevEQ origHash unpackHash
295−
296− testThunkPack' $ fromText repo
297−
298− it "aborts thunk pack when there are uncommitted files" $ inTmpObInitWithImplCopy $ \dir -> do
299− testThunkPack' (dir </> thunk)
300−
301− describe "ob thunk update --branch" $ parallel $ do
302− it "can change a thunk to the latest version of a desired branch" $ withTmp $ \dir -> do
303− let branch1 = "master"
304− branch2 = "develop"
305− run_ gitPath ["clone", "https://github.com/reflex-frp/reflex.git", toTextIgnore dir, "--branch", branch1]
306− runOb_ ["thunk" , "pack", toTextIgnore dir]
307− runOb_ ["thunk", "update", toTextIgnore dir, "--branch", branch2]
308−
309− it "doesn't create anything when given an invalid branch" $ withTmp $ \dir -> do
310− let checkDir dir' = liftIO $ getDirectoryContents $ T.unpack $ toTextIgnore dir'
311− run_ gitPath ["clone", "https://github.com/reflex-frp/reflex.git", toTextIgnore dir, "--branch", "master"]
312− runOb_ ["thunk" , "pack", toTextIgnore dir]
313− startingContents <- checkDir dir
314− void $ errExit False $ runOb ["thunk", "update", toTextIgnore dir, "--branch", "dumble-palooza"]
315− checkDir dir >>= liftIO . assertEqual "" startingContents
316−
317− describe "ob hoogle" $ {- NOT parallel -} do
318− it "starts a hoogle server on the given port" $ inTmpObInit $ \_ -> do
319− [p0] <- liftIO $ getFreePorts 1
320− maskExitSuccess $ runHandle "ob" ["hoogle", "--port", T.pack (show p0)] $ \stdout -> fix $ \loop -> do
321− ln <- liftIO $ T.hGetLine stdout
322− echo ln
323− let search = "Server starting on port " <> T.pack (show p0)
324− case search `T.isInfixOf` ln of
325− False -> loop -- keep waiting
326− True -> do
327− let req uri = liftIO $ HTTP.parseRequest uri >>= flip HTTP.httpLbs httpManager
328− req ("http://127.0.0.1:" <> show p0) >>= \r -> case HTTP.responseStatus r == HTTP.ok200 of
329− False -> errorExit $ "Request to hoogle server failed: " <> T.pack (show r)
330− True -> exit 0
331378
332379maskExitSuccess :: Sh () -> Sh ()
333380maskExitSuccess = handle_sh (\case ExitSuccess -> pure (); e -> throw e)
@@ -350,28 +397,31 @@ testObRunInDir executable extraArgs mdir httpManager = maskExitSuccess $ do
350397 else exit 0
351398
352399testThunkPack :: String -> [Text] -> FilePath -> Sh ()
353−testThunkPack executable args path' = withTempFile (T.unpack $ toTextIgnore path') "test-file" $ \file handle -> do
354− let pack' = readProcessWithExitCode executable (T.unpack <$> ["thunk", "pack", toTextIgnore path'] ++ args) ""
355− ensureThunkPackFails q = liftIO $ pack' >>= \case
356− (code, out, err)
357− | code == ExitSuccess -> fail $ "ob thunk pack succeeded when it should have failed with error '" <> show q <> "'"
358− | q `T.isInfixOf` T.pack (out <> err) -> pure ()
359− | otherwise -> fail $ "ob thunk pack failed for an unexpected reason, expecting '" <> show q <> "', received: " <> show out <> "\nstderr: " <> err
360− git = chdir path' . run gitPath
400+testThunkPack executable args path' = withTempFile repoDir "test-file" $ \file handle -> do
401+ let
402+ pack' = readProcessWithExitCode executable (["thunk", "pack", path'] ++ map T.unpack args) ""
403+ ensureThunkPackFails q = liftIO $ pack' >>= \case
404+ (code, out, err)
405+ | code == ExitSuccess -> fail $ "ob thunk pack succeeded when it should have failed with error '" <> show q <> "'"
406+ | q `T.isInfixOf` T.pack (out <> err) -> pure ()
407+ | otherwise -> fail $ "ob thunk pack failed for an unexpected reason, expecting '" <> show q <> "', received: " <> show out <> "\nstderr: " <> err
408+ git = chdir repoDir . run_ gitPath
361409 -- Untracked files
362410 ensureThunkPackFails "Untracked files"
363− void $ git ["add", T.pack file]
411+ git ["add", T.pack file]
364412 -- Uncommitted files (staged)
365413 ensureThunkPackFails "unsaved"
366− chdir path' $ commit "test commit"
414+ chdir repoDir $ commit "test commit"
367415 -- Non-pushed commits in any branch
368416 ensureThunkPackFails "not yet pushed"
369417 -- Uncommitted files (unstaged)
370418 liftIO $ T.hPutStrLn handle "test file" >> hClose handle
371419 ensureThunkPackFails "modified"
372420 -- Existing stashes
373− void $ git $ gitUserConfig <> [ "stash" ]
421+ git $ gitUserConfig <> [ "stash" ]
374422 ensureThunkPackFails "has stashes"
423+ where
424+ repoDir = path' </> unpackedDirName
375425
376426-- | Blocks until a non-empty line is available
377427hGetLineSkipBlanks :: MonadIO m => Handle -> m Text
@@ -432,3 +482,121 @@ getFreePorts n = Socket.withSocketsDo $ do
432482 sock <- Socket.socket (Socket.addrFamily addr) (Socket.addrSocketType addr) (Socket.addrProtocol addr)
433483 Socket.bind sock (Socket.addrAddress addr)
434484 pure sock
485+
486+testLegacyGitThunks :: Bool -> IO ()
487+testLegacyGitThunks isVerbose = withSystemTempDirectory "test-git-repo" $ \gitDir -> do
488+ isolatedGitShell isVerbose gitDir $ \git -> do
489+ writefile (gitDir </> ("default.nix" :: FilePath)) "{}: \"hello\""
490+ _ <- git ["add", "--all"]
491+ _ <- git ["commit", "-m", "Initial commit"]
492+ rev <- T.strip <$> git ["rev-parse", "HEAD"]
493+ sha256 :: Text
494+ <- either error pure
495+ =<< (Aeson.parseEither (Aeson..: "sha256") <=< Aeson.eitherDecodeStrict . T.encodeUtf8)
496+ <$> run nixPrefetchGitPath [toTextIgnore gitDir]
497+
498+ for_ (legacyGitThunks (GitThunkParams gitDir rev sha256)) $ \mkFiles ->
499+ withSystemTempDirectory "test-thunks" $ \thunkDir -> do
500+ liftIO $ mkFiles thunkDir
501+ run_ "ob" ["thunk", "unpack", toTextIgnore thunkDir]
502+ run_ "ob" ["thunk", "pack", toTextIgnore thunkDir]
503+
504+data GitThunkParams = GitThunkParams
505+ { _gitThunkParams_repo :: !FilePath
506+ , _gitThunkParams_rev :: !Text
507+ , _gitThunkParams_sha256 :: !Text
508+ } deriving Show
509+
510+legacyGitThunks :: GitThunkParams -> [FilePath -> IO ()]
511+legacyGitThunks (GitThunkParams repo' rev sha256) =
512+ [ mkLegacyIO
513+ (T.unlines
514+ [ "# DO NOT HAND-EDIT THIS FILE"
515+ , "let fetchGit = {url, rev, ref ? null, branch ? null, sha256 ? null, fetchSubmodules ? null}:"
516+ , " assert !fetchSubmodules; (import <nixpkgs> {}).fetchgit { inherit url rev sha256; };"
517+ , "in import (fetchGit (builtins.fromJSON (builtins.readFile ./git.json)))"
518+ ]
519+ )
520+ (Map.fromList
521+ [ ("url", repo)
522+ , ("rev", rev)
523+ , ("branch", "master")
524+ , ("sha256", sha256)
525+ ]
526+ )
527+ , mkLegacyIO
528+ (T.unlines
529+ [ "# DO NOT HAND-EDIT THIS FILE"
530+ , "let fetchGit = {url, rev, ref ? null, branch ? null, sha256 ? null, fetchSubmodules ? null}:"
531+ , " if builtins.hasAttr \"fetchGit\" builtins"
532+ , " then builtins.fetchGit ({ inherit url rev; } // (if branch == null then {} else { ref = branch; }))"
533+ , " else abort \"Plain Git repositories are only supported on nix 2.0 or higher.\";"
534+ , "in import (fetchGit (builtins.fromJSON (builtins.readFile ./git.json)))"
535+ ]
536+ )
537+ (Map.fromList
538+ [ ("url", repo)
539+ , ("rev", rev)
540+ , ("branch", "master")
541+ , ("sha256", sha256)
542+ ]
543+ )
544+ , mkLegacyIO
545+ (T.unlines
546+ [ "# DO NOT HAND-EDIT THIS FILE"
547+ , "let fetch = {url, rev, ref ? null, sha256 ? null, fetchSubmodules ? false, private ? false, ...}:"
548+ , " let realUrl = let firstChar = builtins.substring 0 1 url; in"
549+ , " if firstChar == \"/\" then /. + url"
550+ , " else if firstChar == \".\" then ./. + url"
551+ , " else url;"
552+ , " in if !fetchSubmodules && private then builtins.fetchGit {"
553+ , " url = realUrl; inherit rev;"
554+ , " } else (import <nixpkgs> {}).fetchgit {"
555+ , " url = realUrl; inherit rev sha256;"
556+ , " };"
557+ , "in import (fetch (builtins.fromJSON (builtins.readFile ./git.json)))"
558+ ]
559+ )
560+ (Map.fromList
561+ [ ("url", Aeson.String repo)
562+ , ("rev", Aeson.String rev)
563+ , ("branch", Aeson.String "master")
564+ , ("sha256", Aeson.String sha256)
565+ , ("private", Aeson.Bool False)
566+ ]
567+ )
568+ , mkLegacyIO
569+ (T.unlines
570+ [ "# DO NOT HAND-EDIT THIS FILE"
571+ , "let fetch = {url, rev, branch ? null, sha256 ? null, fetchSubmodules ? false, private ? false, ...}:"
572+ , " let realUrl = let firstChar = builtins.substring 0 1 url; in"
573+ , " if firstChar == \"/\" then /. + url"
574+ , " else if firstChar == \".\" then ./. + url"
575+ , " else url;"
576+ , " in if !fetchSubmodules && private then builtins.fetchGit {"
577+ , " url = realUrl; inherit rev;"
578+ , " ${if branch == null then null else \"ref\"} = branch;"
579+ , " } else (import <nixpkgs> {}).fetchgit {"
580+ , " url = realUrl; inherit rev sha256;"
581+ , " };"
582+ , "in import (fetch (builtins.fromJSON (builtins.readFile ./git.json)))"
583+ ]
584+ )
585+ (Map.fromList
586+ [ ("url", Aeson.String repo)
587+ , ("rev", Aeson.String rev)
588+ , ("branch", Aeson.String "master")
589+ , ("sha256", Aeson.String sha256)
590+ , ("private", Aeson.Bool False)
591+ ]
592+ )
593+ ]
594+ where
595+ repo = T.pack repo'
596+ mkLegacyIO :: Aeson.ToJSON v => Text -> Map.Map Text v -> FilePath -> IO ()
597+ mkLegacyIO defaultNix gitJson dir = do
598+ T.writeFile (dir </> ("default.nix" :: FilePath)) defaultNix
599+ LBS.writeFile (dir </> ("git.json" :: FilePath)) (Aeson.encode gitJson)
600+
601+unpackedDirName :: FilePath
602+unpackedDirName = "."
nixpkgs-overlays/default.nixnew file+15
@@ -0,0 +1,15 @@
1+self: super:
2+
3+let
4+ inherit (self) lib;
5+ inherit (import ../dep/gitignore.nix { inherit lib; }) gitignoreSource;
6+in {
7+ obeliskCleanSource = src:
8+ # WARNING: The order of application here seems to matter a great deal to
9+ # how quickly `ghcid` is able to reload changes. As a rule of thumb,
10+ # always apply `gitignoreSource` first.
11+ # See https://github.com/obsidiansystems/obelisk/pull/666 and related.
12+ lib.cleanSource (gitignoreSource src);
13+
14+ obeliskExecutableConfig = self.callPackage ../lib/executable-config {};
15+}