on master help sign in
Merge remote-tracking branch 'origin/qa' into depend-on-reflex-dom-core
3c7fdb7Elliot Cameron
ChangeLog.md+6−2
@@ -4,14 +4,18 @@ This project's release branch is `master`. This log is written from the perspect
4
4
5## Unreleased
5## Unreleased
6
6
7+* Obelisk now depends directly on `reflex-dom-core` instead of `reflex-dom`. This avoids requiring a `jsaddle-webkit2gtk` dependency in apps that are only using `jsaddle-warp`. ([#606](https://github.com/obsidiansystems/obelisk/pull/606))
8+
9+## v0.4.0.0 - 2020-01-10
10+
7* Bump reflex-platform which, notably, bumps nixpkgs to 19.09. ([#585](https://github.com/obsidiansystems/obelisk/pull/585))
11* Bump reflex-platform which, notably, bumps nixpkgs to 19.09. ([#585](https://github.com/obsidiansystems/obelisk/pull/585))
8* Add new thunk loader for Git repositories that supports `file://` Git remotes and supports private repositories via `builtins.fetchGit` for private repositories (when the `git.json` file specifies `"private": true`). ([#594](https://github.com/obsidiansystems/obelisk/pull/594))
12* Add new thunk loader for Git repositories that supports `file://` Git remotes and supports private repositories via `builtins.fetchGit` for private repositories (when the `git.json` file specifies `"private": true`). ([#594](https://github.com/obsidiansystems/obelisk/pull/594))
9* Add a new thunk loader for GitHub repositories that uses `builtins.fetchTarball` for public repositories to increase loader performance and uses `fetchFromGitHub` for private repositories (when the `github.json` file specifies `"private": true`). Note that `fetchFromGitHub` requires some Nix configuration for the Nix builder to access the repository. If `ob thunk pack` fails in this case, use `-v` to see Nix's helpful message. ([#594](https://github.com/obsidiansystems/obelisk/pull/594))
13* Add a new thunk loader for GitHub repositories that uses `builtins.fetchTarball` for public repositories to increase loader performance and uses `fetchFromGitHub` for private repositories (when the `github.json` file specifies `"private": true`). Note that `fetchFromGitHub` requires some Nix configuration for the Nix builder to access the repository. If `ob thunk pack` fails in this case, use `-v` to see Nix's helpful message. ([#594](https://github.com/obsidiansystems/obelisk/pull/594))
10* Add `--public`/`--private` options to `ob thunk pack` to specify if a repository should be treated as a public or private. ([#594](https://github.com/obsidiansystems/obelisk/pull/594))
14* Add `--public`/`--private` options to `ob thunk pack` to specify if a repository should be treated as a public or private. ([#594](https://github.com/obsidiansystems/obelisk/pull/594))
11* Improve error messaging when a dependency doesn't have the expected `.cabal` or `package.yaml` file. ([#597](https://github.com/obsidiansystems/obelisk/pull/597))
15* Improve error messaging when a dependency doesn't have the expected `.cabal` or `package.yaml` file. ([#597](https://github.com/obsidiansystems/obelisk/pull/597))
12* Improve the skeleton in small ways. ([#593](https://github.com/obsidiansystems/obelisk/pull/593), [#589](https://github.com/obsidiansystems/obelisk/pull/589))
16* Improve the skeleton in small ways. ([#593](https://github.com/obsidiansystems/obelisk/pull/593), [#589](https://github.com/obsidiansystems/obelisk/pull/589))
13−* Fix `ob` commands to again support running from any subdirectory of an obelisk project. ([#591](https://github.com/obsidiansystems/obelisk/pull/591))
17+* Fix `ob` commands to again support running from any subdirectory of an obelisk project ([#591](https://github.com/obsidiansystems/obelisk/pull/591))
14−* Obelisk now depends directly on `reflex-dom-core` instead of `reflex-dom`. This avoids requiring a `jsaddle-webkit2gtk` dependency in apps that are only using `jsaddle-warp`. ([#606](https://github.com/obsidiansystems/obelisk/pull/606))
18+* Add `reflex-platform-func` argument to Obelisk's `default.nix`. It defaults to it's prior behavior of using the reflex-platform in in `dep`. ([#612](https://github.com/obsidiansystems/obelisk/pull/612))
15
19
16## v0.3.0.0 - 2019-12-20
20## v0.3.0.0 - 2019-12-20
17
21
README.md+4−3
@@ -23,14 +23,15 @@ Obelisk provides an easy way to develop and deploy your [Reflex](https://github.
231. Set up nix caches
231. Set up nix caches
24 1. If you are running NixOS, add this to `/etc/nixos/configuration.nix`:
24 1. If you are running NixOS, add this to `/etc/nixos/configuration.nix`:
25 ```
25 ```
26− nix.binaryCaches = [ "https://cache.nixos.org/" "https://nixcache.reflex-frp.org" ];
26+ nix.binaryCaches = [ "https://nixcache.reflex-frp.org" ];
27 nix.binaryCachePublicKeys = [ "ryantrinkle.com-1:JJiAKaRv9mWgpVAz8dwewnZe0AzzEAzPkagE9SP5NWI=" ];
27 nix.binaryCachePublicKeys = [ "ryantrinkle.com-1:JJiAKaRv9mWgpVAz8dwewnZe0AzzEAzPkagE9SP5NWI=" ];
28 ```
28 ```
29 and rebuild your NixOS configuration (e.g. `sudo nixos-rebuild switch`).
29 and rebuild your NixOS configuration (e.g. `sudo nixos-rebuild switch`).
30 1. If you are using another operating system or linux distribution, ensure that these lines are present in your Nix configuration file (`/etc/nix/nix.conf` on most systems; [see full list](https://nixos.org/nix/manual/#sec-conf-file)):
30 1. If you are using another operating system or linux distribution, ensure that these lines are present in your Nix configuration file (`/etc/nix/nix.conf` on most systems; [see full list](https://nixos.org/nix/manual/#sec-conf-file)):
31 ```
31 ```
32− substituters = https://cache.nixos.org https://nixcache.reflex-frp.org
32+ binary-caches = https://cache.nixos.org https://nixcache.reflex-frp.org
33− trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= ryantrinkle.com-1:JJiAKaRv9mWgpVAz8dwewnZe0AzzEAzPkagE9SP5NWI=
33+ binary-cache-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= ryantrinkle.com-1:JJiAKaRv9mWgpVAz8dwewnZe0AzzEAzPkagE9SP5NWI=
34+ binary-caches-parallel-connections = 40
34 ```
35 ```
35 * other Linux: enable sandboxing (see these [issue172](https://github.com/obsidiansystems/obelisk/issues/172#issuecomment-411507818) or [issue6](https://github.com/obsidiansystems/obelisk/issues/6) if you run into build problems)
36 * other Linux: enable sandboxing (see these [issue172](https://github.com/obsidiansystems/obelisk/issues/172#issuecomment-411507818) or [issue6](https://github.com/obsidiansystems/obelisk/issues/6) if you run into build problems)
36 ```
37 ```
all-builds.nixnew file+117
@@ -0,0 +1,117 @@
1+{ self-args ? {
2+ config.android_sdk.accept_license = true;
3+ iosSdkVersion = "10.2";
4+ }
5+, local-self ? import ./. self-args
6+, supportedSystems ? [
7+ "x86_64-linux"
8+ "x86_64-darwin"
9+ ]
10+}:
11+
12+let
13+ inherit (local-self.nixpkgs) lib runCommand nix;
14+ cacheBuildSystems = supportedSystems;
15+
16+ obeliskPackagesCommon = [
17+ "obelisk-frontend"
18+ "obelisk-route"
19+ "obelisk-executable-config-lookup"
20+ ];
21+
22+ obeliskPackagesBackend = obeliskPackagesCommon ++ [
23+ "obelisk-asset-manifest"
24+ "obelisk-asset-serve-snap"
25+ "obelisk-backend"
26+ "obelisk-cliapp"
27+ "obelisk-command"
28+ "obelisk-executable-config-inject"
29+ "obelisk-frontend"
30+ "obelisk-run"
31+ "obelisk-route"
32+ "obelisk-selftest"
33+ "obelisk-snap-extras"
34+ ];
35+
36+ pnameToAttrs = pkgsSet: pnames:
37+ lib.listToAttrs (map
38+ (name: { inherit name; value = pkgsSet.${name}; })
39+ pnames);
40+
41+ concatDepends = let
42+ extractDeps = x: (x.override {
43+ mkDerivation = drv: {
44+ out = builtins.concatLists [
45+ (drv.buildDepends or [])
46+ (drv.libraryHaskellDepends or [])
47+ (drv.executableHaskellDepends or [])
48+ ];
49+ };
50+ }).out;
51+ in pkgAttrs: builtins.concatLists (map extractDeps (builtins.attrValues pkgAttrs));
52+
53+ perPlatform = lib.genAttrs cacheBuildSystems (system: let
54+ reflex-platform = import ./dep/reflex-platform { inherit system; };
55+ perProfiling = profiling: let
56+ obelisk = import ./. (self-args // { inherit system profiling; });
57+ ghc = pnameToAttrs
58+ obelisk.haskellPackageSets.ghc
59+ obeliskPackagesBackend;
60+ ghcjs = pnameToAttrs
61+ obelisk.haskellPackageSets.ghcjs
62+ obeliskPackagesCommon;
63+ cachePackages = builtins.concatLists [
64+ (builtins.attrValues ghc)
65+ (lib.optionals (!profiling) (builtins.attrValues ghcjs))
66+ (concatDepends ghc)
67+ (lib.optionals (!profiling) (concatDepends ghcjs))
68+ (lib.optional reflex-platform.androidSupport androidSkeleton)
69+ (lib.optional reflex-platform.iosSupport iosSkeleton)
70+ (lib.optionals (!profiling) [
71+ command
72+ serverSkeletonExe
73+ serverSkeletonShell
74+ ])
75+ ];
76+ command = obelisk.command;
77+ skeleton = import ./skeleton { inherit obelisk; };
78+ serverSkeletonExe = skeleton.exe;
79+ # TODO fix nixpkgs so it doesn't try to run the result of haskell shells as setup hooks.
80+ serverSkeletonShell = local-self.nixpkgs.runCommand "shell-safe-for-dep" {} ''
81+ touch "$out"
82+ echo "return" >> "$out"
83+ cat "${skeleton.shells.ghc}" >> "$out"
84+ '';
85+ androidSkeleton = (import ./skeleton { inherit obelisk; }).android.frontend;
86+ iosSkeleton = (import ./skeleton { inherit obelisk; }).ios.frontend;
87+ nameSuffix = if profiling then "profiled" else "unprofiled";
88+ in {
89+ inherit
90+ ghc
91+ ;
92+ cache = reflex-platform.pinBuildInputs "obelisk-${system}-${nameSuffix}" cachePackages;
93+ } // lib.optionalAttrs (!profiling) {
94+ inherit
95+ command
96+ ghcjs
97+ serverSkeletonExe
98+ serverSkeletonShell
99+ ;
100+ } // lib.optionalAttrs reflex-platform.androidSupport {
101+ inherit androidSkeleton;
102+ } // lib.optionalAttrs reflex-platform.iosSupport {
103+ inherit iosSkeleton;
104+ };
105+ profiled = perProfiling true;
106+ unprofiled = perProfiling false;
107+ cachePackages = map (p: p.cache) [ profiled unprofiled ];
108+ in {
109+ inherit profiled unprofiled;
110+ cache = reflex-platform.pinBuildInputs "obelisk-${system}" cachePackages;
111+ });
112+
113+ metaCache = local-self.reflex-platform.pinBuildInputs
114+ "obelisk-everywhere"
115+ (map (a: a.cache) (builtins.attrValues perPlatform));
116+
117+in perPlatform // { inherit metaCache; }
all-tests.nixnew file+145
@@ -0,0 +1,145 @@
1+{
2+ supportedSystems ? [ builtins.currentSystem ]
3+}:
4+let
5+ nginxRoot = "/run/nginx";
6+ obelisk = import ./default.nix {};
7+ # Get NixOS a pre-release 20.03 that contains the python based tests and recursive nix
8+ pkgs = import (builtins.fetchTarball https://github.com/nixos/nixpkgs/archive/3de5266.tar.gz) {};
9+ sshKeys = import (pkgs.path + /nixos/tests/ssh-keys.nix) pkgs;
10+ make-test = import (pkgs.path + /nixos/tests/make-test-python.nix);
11+ obelisk-everywhere = (import ./all-builds.nix { inherit supportedSystems; }).x86_64-linux.cache;
12+ snakeOilPrivateKey = sshKeys.snakeOilPrivateKey.text;
13+ snakeOilPublicKey = sshKeys.snakeOilPublicKey;
14+in
15+ make-test ({...}: {
16+ name = "obelisk";
17+ nodes = {
18+ githost = {
19+ networking.firewall.allowedTCPPorts = [ 22 80 ];
20+ services.openssh = {
21+ enable = true;
22+ };
23+ environment.systemPackages = [
24+ pkgs.git
25+ ];
26+ users.users.root.openssh.authorizedKeys.keys = [
27+ snakeOilPublicKey
28+ ];
29+ };
30+
31+ client = {
32+ imports = [
33+ (pkgs.path + /nixos/modules/installer/cd-dvd/channel.nix)
34+ ];
35+ nix.useSandbox = false;
36+ nix.binaryCaches = [];
37+ environment.systemPackages = [
38+ obelisk.command
39+ obelisk.shell
40+ obelisk-everywhere
41+ pkgs.git
42+ ];
43+ };
44+ };
45+
46+ testScript =
47+ let
48+ privateKeyFile = pkgs.writeText "id_rsa" ''${snakeOilPrivateKey}'';
49+ thunkableSample = pkgs.writeText "default.nix" ''
50+ let pkgs = import <nixpkgs> {}; in pkgs.git
51+ '';
52+ invalidThunkableSample = pkgs.writeText "default.nix" ''
53+ let pkgs = import <nixpkgs> {}; in pkgtypo.git
54+ '';
55+ sshConfigFile = pkgs.writeText "ssh_config" ''
56+ Host *
57+ StrictHostKeyChecking no
58+ UserKnownHostsFile=/dev/null
59+ ConnectionAttempts=1
60+ ConnectTimeout=1
61+ IdentityFile=~/.ssh/id_rsa
62+ User=root
63+ '';
64+ in ''
65+ start_all()
66+ githost.wait_for_open_port("22")
67+
68+ with subtest("test obelisk is installed"):
69+ client.succeed("ob --help")
70+
71+ with subtest("test the client can access the server via ssh"):
72+ client.succeed("mkdir -p ~/.ssh/")
73+ client.succeed(
74+ "cp ${privateKeyFile} ~/.ssh/id_rsa"
75+ )
76+ client.succeed("chmod 600 ~/.ssh/id_rsa")
77+ client.wait_until_succeeds(
78+ "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa githost true"
79+ )
80+ client.succeed(
81+ "cp ${sshConfigFile} ~/.ssh/config"
82+ )
83+ client.wait_until_succeeds("ssh githost true")
84+
85+ with subtest("test a remote bare repo can be started"):
86+ githost.succeed("mkdir -p ~/myorg/myapp.git")
87+ githost.succeed("cd ~/myorg/myapp.git && git init --bare")
88+
89+ with subtest("test a git project can be configured with a remote using ssh"):
90+ client.succeed("mkdir -p ~/code/myapp")
91+ client.succeed("cd ~/code/myapp && git init")
92+ client.succeed(
93+ "cp ${thunkableSample} ~/code/myapp/default.nix"
94+ )
95+ client.succeed("cd ~/code/myapp && git add .")
96+ client.succeed('git config --global user.email "you@example.com"')
97+ client.succeed('git config --global user.name "Your Name"')
98+ client.succeed('cd ~/code/myapp && git commit -m "Initial"')
99+ client.succeed(
100+ "cd ~/code/myapp && git remote add origin root@githost:/root/myorg/myapp.git"
101+ )
102+
103+
104+ with subtest("test pushing code to the remote"):
105+ client.succeed("cd ~/code/myapp && git push -u origin master")
106+ client.succeed("cd ~/code/myapp && git status")
107+
108+ with subtest("test obelisk can pack"):
109+ client.succeed("ob -v thunk pack ~/code/myapp")
110+ client.succeed("grep -qF 'git' ~/code/myapp/default.nix")
111+ client.succeed("grep -qF 'myorg' ~/code/myapp/git.json")
112+ client.succeed("ob -v thunk unpack ~/code/myapp")
113+
114+ with subtest("test obelisk can set the public / private flag"):
115+ client.succeed("ob -v thunk pack ~/code/myapp --private")
116+ client.fail("""grep -qF '"private": practice' ~/code/myapp/git.json""")
117+ client.succeed("""grep -qF '"private": true' ~/code/myapp/git.json""")
118+ client.succeed("nix-build ~/code/myapp")
119+ client.succeed("ob -v thunk unpack ~/code/myapp")
120+ client.succeed("ob -v thunk pack ~/code/myapp --public")
121+ client.succeed("""grep -qF '"private": false' ~/code/myapp/git.json""")
122+ client.succeed("nix-build ~/code/myapp")
123+ client.succeed("ob -v thunk unpack ~/code/myapp")
124+
125+ with subtest("test building an invalid thunk fails"):
126+ client.succeed("cd ~/code/myapp && git checkout -b bad")
127+ client.succeed(
128+ "cp ${invalidThunkableSample} ~/code/myapp/default.nix"
129+ )
130+ client.succeed("cd ~/code/myapp && git add .")
131+ client.succeed('git config --global user.email "you@example.com"')
132+ client.succeed('git config --global user.name "Your Name"')
133+ client.succeed('cd ~/code/myapp && git commit -m "Bad commit"')
134+ client.succeed("cd ~/code/myapp && git push -u origin bad")
135+ client.succeed("ob -v thunk pack ~/code/myapp --public")
136+ client.fail("nix-build ~/code/myapp")
137+ client.succeed("ob -v thunk unpack ~/code/myapp")
138+ client.succeed("cd ~/code/myapp && git checkout master")
139+
140+ with subtest("test obelisk can detect private repos"):
141+ client.succeed("ob -v thunk pack ~/code/myapp")
142+ client.succeed("""grep -qF '"private": false' ~/code/myapp/git.json""")
143+ client.succeed("ob -v thunk unpack ~/code/myapp")
144+ '';
145+ }) {}
default.nix+21−11
@@ -2,6 +2,7 @@
2, profiling ? false
2, profiling ? false
3, iosSdkVersion ? "10.2"
3, iosSdkVersion ? "10.2"
4, config ? {}
4, config ? {}
5+, reflex-platform-func ? import ./dep/reflex-platform
5}:
6}:
6let
7let
7 cleanSource = builtins.filterSource (name: _: let baseName = builtins.baseNameOf name; in !(
8 cleanSource = builtins.filterSource (name: _: let baseName = builtins.baseNameOf name; in !(
@@ -16,7 +17,7 @@ let
16 openssh
17 openssh
17 ];
18 ];
18
19
19− getReflexPlatform = sys: import ./dep/reflex-platform {
20+ getReflexPlatform = sys: reflex-platform-func {
20 inherit iosSdkVersion config;
21 inherit iosSdkVersion config;
21 system = sys;
22 system = sys;
22 enableLibraryProfiling = profiling;
23 enableLibraryProfiling = profiling;
@@ -47,6 +48,8 @@ let
47 pkgs = self.callPackage ({ pkgs }: pkgs) {};
48 pkgs = self.callPackage ({ pkgs }: pkgs) {};
48 onLinux = pkg: f: if pkgs.stdenv.isLinux then f pkg else pkg;
49 onLinux = pkg: f: if pkgs.stdenv.isLinux then f pkg else pkg;
49 in {
50 in {
51+ shelly = self.callHackage "shelly" "1.9.0" {};
52+
50 obelisk-executable-config-inject = pkgs.obeliskExecutableConfig.platforms.web.inject self;
53 obelisk-executable-config-inject = pkgs.obeliskExecutableConfig.platforms.web.inject self;
51
54
52 obelisk-asset-manifest = self.callCabal2nix "obelisk-asset-manifest" (cleanSource ./lib/asset/manifest) {};
55 obelisk-asset-manifest = self.callCabal2nix "obelisk-asset-manifest" (cleanSource ./lib/asset/manifest) {};
@@ -64,7 +67,12 @@ let
64 haskellLib.overrideCabal pkg (drv: { librarySystemDepends = [ pkgs.iproute ]; })
67 haskellLib.overrideCabal pkg (drv: { librarySystemDepends = [ pkgs.iproute ]; })
65 );
68 );
66 obelisk-route = self.callCabal2nix "obelisk-route" (cleanSource ./lib/route) {};
69 obelisk-route = self.callCabal2nix "obelisk-route" (cleanSource ./lib/route) {};
67− obelisk-selftest = self.callCabal2nix "obelisk-selftest" (cleanSource ./lib/selftest) {};
70+ obelisk-selftest = haskellLib.overrideCabal (self.callCabal2nix "obelisk-selftest" (cleanSource ./lib/selftest) {}) {
71+ librarySystemDepends = [
72+ pkgs.cabal-install
73+ pkgs.coreutils
74+ ];
75+ };
68 obelisk-snap-extras = self.callCabal2nix "obelisk-snap-extras" (cleanSource ./lib/snap-extras) {};
76 obelisk-snap-extras = self.callCabal2nix "obelisk-snap-extras" (cleanSource ./lib/snap-extras) {};
69 tabulation = self.callCabal2nix "tabulation" (cleanSource ./lib/tabulation) {};
77 tabulation = self.callCabal2nix "tabulation" (cleanSource ./lib/tabulation) {};
70 })
78 })
@@ -147,6 +155,7 @@ in rec {
147 compressedJs = frontend: optimizationLevel: pkgs.runCommand "compressedJs" {} ''
155 compressedJs = frontend: optimizationLevel: pkgs.runCommand "compressedJs" {} ''
148 mkdir $out
156 mkdir $out
149 cd $out
157 cd $out
158+ # TODO profiling + static shouldn't break and need an ad-hoc workaround like that
150 ln -s "${haskellLib.justStaticExecutables frontend}/bin/frontend.jsexe/all.js" all.unminified.js
159 ln -s "${haskellLib.justStaticExecutables frontend}/bin/frontend.jsexe/all.js" all.unminified.js
151 ${if optimizationLevel == null then ''
160 ${if optimizationLevel == null then ''
152 ln -s all.unminified.js all.js
161 ln -s all.unminified.js all.js
@@ -229,7 +238,7 @@ in rec {
229 pkgs.runCommand "serverExe" {} ''
238 pkgs.runCommand "serverExe" {} ''
230 mkdir $out
239 mkdir $out
231 set -eux
240 set -eux
232− ln -s "${haskellLib.justStaticExecutables backend}"/bin/* $out/
241+ ln -s "${if profiling then backend else haskellLib.justStaticExecutables backend}"/bin/* $out/
233 ln -s "${mkAssets assets}" $out/static.assets
242 ln -s "${mkAssets assets}" $out/static.assets
234 ln -s ${mkAssets (compressedJs frontend optimizationLevel)} $out/frontend.jsexe.assets
243 ln -s ${mkAssets (compressedJs frontend optimizationLevel)} $out/frontend.jsexe.assets
235 echo ${version} > $out/version
244 echo ${version} > $out/version
@@ -327,18 +336,19 @@ in rec {
327 passthru = { inherit android ios packages overrides tools shellToolOverrides withHoogle staticFiles staticFilesImpure __closureCompilerOptimizationLevel processedStatic __iosWithConfig __androidWithConfig; };
336 passthru = { inherit android ios packages overrides tools shellToolOverrides withHoogle staticFiles staticFilesImpure __closureCompilerOptimizationLevel processedStatic __iosWithConfig __androidWithConfig; };
328 };
337 };
329 in mkProject (projectDefinition args));
338 in mkProject (projectDefinition args));
339+ mainProjectOut = projectOut system;
330− serverOn = sys: version: serverExe
340+ serverOn = projectInst: version: serverExe
331− (projectOut sys).ghc.backend
341+ projectInst.ghc.backend
332− (projectOut system).ghcjs.frontend
342+ mainProjectOut.ghcjs.frontend
333− (projectOut sys).passthru.staticFiles
343+ projectInst.passthru.staticFiles
334− (projectOut sys).passthru.__closureCompilerOptimizationLevel
344+ projectInst.passthru.__closureCompilerOptimizationLevel
335 version;
345 version;
336− linuxExe = serverOn "x86_64-linux";
346+ linuxExe = serverOn (projectOut "x86_64-linux");
337 dummyVersion = "Version number is only available for deployments";
347 dummyVersion = "Version number is only available for deployments";
338− in projectOut system // {
348+ in mainProjectOut // {
339 linuxExeConfigurable = linuxExe;
349 linuxExeConfigurable = linuxExe;
340 linuxExe = linuxExe dummyVersion;
350 linuxExe = linuxExe dummyVersion;
341− exe = serverOn system dummyVersion;
351+ exe = serverOn mainProjectOut dummyVersion;
342 server = args@{ hostName, adminEmail, routeHost, enableHttps, version }:
352 server = args@{ hostName, adminEmail, routeHost, enableHttps, version }:
343 server (args // { exe = linuxExe version; });
353 server (args // { exe = linuxExe version; });
344 obelisk = import (base + "/.obelisk/impl") {};
354 obelisk = import (base + "/.obelisk/impl") {};
dep/reflex-platform/github.json+3−3
@@ -1,7 +1,7 @@
1{
1{
2 "owner": "reflex-frp",
2 "owner": "reflex-frp",
3 "repo": "reflex-platform",
3 "repo": "reflex-platform",
4− "branch": "master",
4+ "branch": "develop",
5− "rev": "72ca177da290dc5311c08683b8d167a205c9322e",
5+ "rev": "90351bdb2c99d416ea95fd5b8b474e8f9fab2942",
6− "sha256": "06pqml1iqc5fawwsqb7wq77gh5v07bgb92hz8r1hf0ysr0abafcg"
6+ "sha256": "0bbzz4ckdx0ilhhpd3drfbsc0zdb2rnhw2m3nls9villbs3lz3lh"
7}
7}
jobsets.nixdeleted−71
@@ -1,71 +0,0 @@
1−{ prs }:
2−
3−let
4− self = import ./. {};
5− pkgs = self.nixpkgs;
6− mkFetchGithub = value: {
7− inherit value;
8− type = "git";
9− emailresponsible = false;
10− };
11−in
12−with pkgs.lib;
13−let
14− defaults = jobs: {
15− inherit (jobs) description;
16− enabled = 1;
17− hidden = false;
18− keepnr = 10;
19− schedulingshares = 100;
20− checkinterval = 120;
21− enableemail = false;
22− emailoverride = "";
23− nixexprinput = "obelisk";
24− nixexprpath = "release.nix";
25− inputs = jobs.inputs // {
26− nixpkgs = {
27− type = "git";
28− value = "https://github.com/NixOS/nixpkgs-channels nixos-unstable";
29− emailresponsible = false;
30− };
31− };
32− self-args = {
33− type = "nix";
34− value = "{ config.android_sdk.accept_license = true; }";
35− emailresponsible = false;
36− };
37− };
38− branchJobset = branch: defaults {
39− description = "obelisk-${branch}";
40− inputs = {
41− obelisk = {
42− value = "https://github.com/obsidiansystems/obelisk ${branch}";
43− type = "git";
44− emailresponsible = false;
45− };
46− };
47− };
48− makePr = num: info: {
49− name = "obelisk-pr-${num}";
50− value = defaults {
51− description = "#${num}: ${info.title}";
52− inputs = {
53− obelisk = {
54− #NOTE: This should really use "pull/${num}/merge"; however, GitHub's
55− #status checks only operate on PR heads. This creates a race
56− #condition, which can currently only be solved by requiring PRs to be
57− #up to date before they're merged. See
58− #https://github.com/isaacs/github/issues/1002
59− value = "https://github.com/obsidiansystems/obelisk pull/${num}/head";
60− type = "git";
61− emailresponsible = false;
62− };
63− };
64− };
65− };
66− processedPrs = mapAttrs' makePr (builtins.fromJSON (builtins.readFile prs));
67− jobsetsAttrs = processedPrs //
68− genAttrs ["master"] branchJobset;
69−in {
70− jobsets = pkgs.writeText "spec.json" (builtins.toJSON jobsetsAttrs);
71−}
lib/command/obelisk-command.cabal+1−1
@@ -1,5 +1,5 @@
1name: obelisk-command
1name: obelisk-command
2−version: 0.3
2+version: 0.4
3cabal-version: >= 1.8
3cabal-version: >= 1.8
4build-type: Simple
4build-type: Simple
5
5
lib/command/src/Obelisk/Command/Deploy.hs+2−1
@@ -41,6 +41,7 @@ import Obelisk.Command.Project
41import Obelisk.Command.Thunk
41import Obelisk.Command.Thunk
42import Obelisk.Command.Utils
42import Obelisk.Command.Utils
43
43
44+
44deployInit
45deployInit
45 :: MonadObelisk m
46 :: MonadObelisk m
46 => ThunkPtr
47 => ThunkPtr
@@ -57,7 +58,7 @@ deployInit thunkPtr deployDir sshKeyPath hostnames route adminEmail enableHttps
57 False -> failWith $ T.pack $ "ob deploy init: file does not exist: " <> sshKeyPath
58 False -> failWith $ T.pack $ "ob deploy init: file does not exist: " <> sshKeyPath
58 True -> pure $ deployDir </> "ssh_key"
59 True -> pure $ deployDir </> "ssh_key"
59 callProcessAndLogOutput (Notice, Error) $
60 callProcessAndLogOutput (Notice, Error) $
60− proc "cp" [sshKeyPath, localKey]
61+ proc cp [sshKeyPath, localKey]
61 liftIO $ setFileMode localKey $ ownerReadMode .|. ownerWriteMode
62 liftIO $ setFileMode localKey $ ownerReadMode .|. ownerWriteMode
62 return localKey
63 return localKey
63 withSpinner "Validating configuration" $ do
64 withSpinner "Validating configuration" $ do
lib/command/src/Obelisk/Command/Run.hs+66−61
@@ -4,13 +4,17 @@
4{-# LANGUAGE OverloadedStrings #-}
4{-# LANGUAGE OverloadedStrings #-}
5{-# LANGUAGE ScopedTypeVariables #-}
5{-# LANGUAGE ScopedTypeVariables #-}
6{-# LANGUAGE TemplateHaskell #-}
6{-# LANGUAGE TemplateHaskell #-}
7+{-# LANGUAGE TupleSections #-}
7module Obelisk.Command.Run where
8module Obelisk.Command.Run where
8
9
10+import Control.Applicative (liftA2)
9−import Control.Exception (bracket)
11+import Control.Exception (Exception, bracket)
10import Control.Monad
12import Control.Monad
13+import Control.Monad.Except (runExceptT, throwError)
11import Control.Monad.IO.Class (liftIO)
14import Control.Monad.IO.Class (liftIO)
12import Control.Monad.Reader (MonadIO)
15import Control.Monad.Reader (MonadIO)
13import Data.Either
16import Data.Either
17+import Data.Foldable (for_)
14import qualified Data.List as L
18import qualified Data.List as L
15import qualified Data.List.NonEmpty as NE
19import qualified Data.List.NonEmpty as NE
16import Data.Maybe
20import Data.Maybe
@@ -24,22 +28,18 @@ import Distribution.Types.CondTree
24import Distribution.Types.GenericPackageDescription
28import Distribution.Types.GenericPackageDescription
25import Distribution.Types.Library
29import Distribution.Types.Library
26import Distribution.Utils.Generic
30import Distribution.Utils.Generic
27−import Hpack.Config
31+import qualified Hpack.Config as Hpack
28−import Hpack.Render
32+import qualified Hpack.Render as Hpack
29−import Hpack.Yaml
33+import qualified Hpack.Yaml as Hpack
30import Language.Haskell.Extension
34import Language.Haskell.Extension
31import Network.Socket hiding (Debug)
35import Network.Socket hiding (Debug)
32import System.Directory
36import System.Directory
33import System.FilePath
37import System.FilePath
34import System.IO.Temp (withSystemTempDirectory)
38import System.IO.Temp (withSystemTempDirectory)
35import System.Which (staticWhich)
39import System.Which (staticWhich)
36−import Data.ByteString (ByteString)
37
40
38import Obelisk.App (MonadObelisk, ObeliskT)
41import Obelisk.App (MonadObelisk, ObeliskT)
39−import Obelisk.CliApp
40− ( CliT (..), HasCliConfig, Severity (..)
41− , callCommand, failWith, getCliConfig, putLog, proc
42+import Obelisk.CliApp (Severity (..) , callCommand, failWith, putLog, proc, readProcessAndLogStderr)
42− , readProcessAndLogStderr, runCli)
43import Obelisk.Command.Project (inProjectShell, withProjectRoot)
43import Obelisk.Command.Project (inProjectShell, withProjectRoot)
44
44
45data CabalPackageInfo = CabalPackageInfo
45data CabalPackageInfo = CabalPackageInfo
@@ -105,6 +105,27 @@ runWatch = do
105 pkgs <- getLocalPkgs
105 pkgs <- getLocalPkgs
106 withGhciScript pkgs $ \dotGhciPath -> runGhcid dotGhciPath Nothing
106 withGhciScript pkgs $ \dotGhciPath -> runGhcid dotGhciPath Nothing
107
107
108+data GuessPackageFileError = GuessPackageFileError_Ambiguous [FilePath] | GuessPackageFileError_NotFound
109+ deriving (Eq, Ord, Show)
110+instance Exception GuessPackageFileError
111+
112+-- | Given a directory, try to guess what the appropriate @.cabal@ or @package.yaml@ file is for the package.
113+guessCabalPackageFile
114+ :: MonadIO m
115+ => FilePath -- ^ Directory to search for cabal package
116+ -> m (Either GuessPackageFileError (Either FilePath FilePath))
117+ -- ^ 'Right' 'Left' for hpack package, 'Right' 'Right' for cabal package; 'FilePath' is relative to given directory.
118+guessCabalPackageFile dir = do
119+ candidates <- liftIO $
120+ filterM (doesFileExist . (dir </>))
121+ =<< filter (liftA2 (||) (== Hpack.packageConfig) (".cabal" `L.isSuffixOf`))
122+ <$> listDirectory dir
123+ pure $ case L.partition (== Hpack.packageConfig) candidates of
124+ ([hpack], _) -> Right $ Left hpack
125+ ([], [cabal]) -> Right $ Right cabal
126+ ([], []) -> Left GuessPackageFileError_NotFound
127+ (hpacks, cabals) -> Left $ GuessPackageFileError_Ambiguous $ hpacks <> cabals
128+
108-- | Relative paths to local packages of an obelisk project
129-- | Relative paths to local packages of an obelisk project
109-- TODO a way to query this
130-- TODO a way to query this
110getLocalPkgs :: Applicative f => f [FilePath]
131getLocalPkgs :: Applicative f => f [FilePath]
@@ -114,54 +135,38 @@ parseCabalPackage
114 :: (MonadObelisk m)
135 :: (MonadObelisk m)
115 => FilePath -- ^ package directory
136 => FilePath -- ^ package directory
116 -> m (Maybe CabalPackageInfo)
137 -> m (Maybe CabalPackageInfo)
117−parseCabalPackage dir = do
138+parseCabalPackage dir = either ((Nothing <$) . putLog Error) (pure . Just) <=< runExceptT $ do
118− let cabalFp = dir </> (takeBaseName dir <> ".cabal")
139+ (cabalContents, packageFile) <- guessCabalPackageFile dir >>= \case
119− hpackFp = dir </> "package.yaml"
140+ Left GuessPackageFileError_NotFound -> throwError $ "No .cabal or package.yaml file found in " <> T.pack dir
120− hasCabal <- liftIO $ doesFileExist cabalFp
141+ Left (GuessPackageFileError_Ambiguous _) -> throwError $ "Unable to determine which .cabal file to use in " <> T.pack dir
121− hasHpack <- liftIO $ doesFileExist hpackFp
142+ Right (Right cabalFileName) -> let file = dir </> cabalFileName in (, file) <$> liftIO (readUTF8File file)
122−
143+ Right (Left hpackFileName) -> do
123− mCabalContents <- if hasCabal
144+ let
124− then Just <$> liftIO (readUTF8File cabalFp)
145+ file = dir </> hpackFileName
125− else if hasHpack
126− then do
127− let decodeOptions = DecodeOptions (ProgramName "ob") hpackFp Nothing decodeYaml
146+ decodeOptions = Hpack.DecodeOptions (Hpack.ProgramName "ob") file Nothing Hpack.decodeYaml
128− liftIO (readPackageConfig decodeOptions) >>= \case
147+ liftIO (Hpack.readPackageConfig decodeOptions) >>= \case
129− Left err -> do
130− putLog Error $ T.pack $ "Failed to parse " <> hpackFp <> ": " <> err
148+ Left err -> throwError $ T.pack $ "Failed to parse " <> file <> ": " <> err
131− return Nothing
132− Right (DecodeResult hpackPackage _ _ _) -> do
149+ Right (Hpack.DecodeResult hpackPackage _ _ _) -> pure (Hpack.renderPackage [] hpackPackage, file)
133− return $ Just $ renderPackage [] hpackPackage
134− else do
135− putLog Error $ T.pack "Found neither cabal nor hpack file"
136− return Nothing
137−
138− fmap join $ forM mCabalContents $ \cabalContents -> do
139− let (warnings, result) = runParseResult $ parseGenericPackageDescription $
140− toUTF8BS cabalContents
141− mapM_ (putLog Warning) $ fmap (T.pack . show) warnings
142− case result of
143− Right gpkg -> do
144− return $ do
145− (_, lib) <- simplifyCondTree (const $ pure True) <$> condLibrary gpkg
146− pure $ CabalPackageInfo
147− { _cabalPackageInfo_packageRoot = takeDirectory cabalFp
148− , _cabalPackageInfo_sourceDirs =
149− fromMaybe (pure ".") $ NE.nonEmpty $ hsSourceDirs $ libBuildInfo lib
150− , _cabalPackageInfo_defaultExtensions =
151− defaultExtensions $ libBuildInfo lib
152− , _cabalPackageInfo_defaultLanguage =
153− defaultLanguage $ libBuildInfo lib
154− , _cabalPackageInfo_compilerOptions = options $ libBuildInfo lib
155− }
156− Left (_, errors) -> do
157− putLog Error $ T.pack $ "Failed to parse " <> cabalFp <> ":"
158− mapM_ (putLog Error) $ fmap (T.pack . show) errors
159− return Nothing
160
150
161−withUTF8FileContentsM :: (MonadIO m, HasCliConfig e m) => FilePath -> (ByteString -> CliT e IO a) -> m a
151+ let (warnings, result) = runParseResult $ parseGenericPackageDescription $
162−withUTF8FileContentsM fp f = do
152+ toUTF8BS cabalContents
163− c <- getCliConfig
153+ for_ warnings $ putLog Warning . T.pack . show
164− liftIO $ withUTF8FileContents fp $ runCli c . f . toUTF8BS
154+ case condLibrary <$> result of
155+ Right (Just condLib) -> do
156+ let (_, lib) = simplifyCondTree (const $ pure True) condLib
157+ pure $ CabalPackageInfo
158+ { _cabalPackageInfo_packageRoot = dir
159+ , _cabalPackageInfo_sourceDirs =
160+ fromMaybe (pure ".") $ NE.nonEmpty $ hsSourceDirs $ libBuildInfo lib
161+ , _cabalPackageInfo_defaultExtensions =
162+ defaultExtensions $ libBuildInfo lib
163+ , _cabalPackageInfo_defaultLanguage =
164+ defaultLanguage $ libBuildInfo lib
165+ , _cabalPackageInfo_compilerOptions = options $ libBuildInfo lib
166+ }
167+ Right Nothing -> throwError "Haskell package has no library component"
168+ Left (_, errors) ->
169+ throwError $ T.pack $ "Failed to parse " <> packageFile <> ":\n" <> unlines (map show errors)
165
170
166-- | Create ghci configuration to load the given packages
171-- | Create ghci configuration to load the given packages
167withGhciScript
172withGhciScript
@@ -169,14 +174,14 @@ withGhciScript
169 => [FilePath] -- ^ List of packages to load into ghci
174 => [FilePath] -- ^ List of packages to load into ghci
170 -> (FilePath -> m ()) -- ^ Action to run with the path to generated temporary .ghci
175 -> (FilePath -> m ()) -- ^ Action to run with the path to generated temporary .ghci
171 -> m ()
176 -> m ()
172−withGhciScript pkgs f = do
177+withGhciScript dirs f = do
173− (pkgDirErrs, packageInfos) <- fmap partitionEithers $ forM pkgs $ \pkg -> do
178+ (pkgDirErrs, packageInfos) <- fmap partitionEithers $ forM dirs $ \dir -> do
174− flip fmap (parseCabalPackage pkg) $ \case
179+ flip fmap (parseCabalPackage dir) $ \case
175− Nothing -> Left pkg
180+ Nothing -> Left dir
176 Just packageInfo -> Right packageInfo
181 Just packageInfo -> Right packageInfo
177
182
178 when (null packageInfos) $
183 when (null packageInfos) $
179− failWith $ T.pack $ "No valid pkgs found in " <> intercalate ", " pkgs
184+ failWith $ T.pack $ "No valid pkgs found in " <> intercalate ", " dirs
180 unless (null pkgDirErrs) $
185 unless (null pkgDirErrs) $
181 putLog Warning $ T.pack $ "Failed to find pkgs in " <> intercalate ", " pkgDirErrs
186 putLog Warning $ T.pack $ "Failed to find pkgs in " <> intercalate ", " pkgDirErrs
182
187
lib/command/src/Obelisk/Command/Thunk.hs+3−3
@@ -635,13 +635,13 @@ updateThunk p f = withSystemTempDirectory "obelisk-thunkptr-" $ \tmpDir -> do
635 Right (ThunkData_Packed _) -> do
635 Right (ThunkData_Packed _) -> do
636 let tmpThunk = tmpDir </> "thunk"
636 let tmpThunk = tmpDir </> "thunk"
637 callProcessAndLogOutput (Notice, Error) $
637 callProcessAndLogOutput (Notice, Error) $
638− proc "cp" ["-r", "-T", thunkDir, tmpThunk]
638+ proc cp ["-r", "-T", thunkDir, tmpThunk]
639 return tmpThunk
639 return tmpThunk
640 Right _ -> failWith "Thunk is not packed"
640 Right _ -> failWith "Thunk is not packed"
641 updateThunkFromTmp p' = do
641 updateThunkFromTmp p' = do
642 _ <- packThunk' True (ThunkPackConfig False (ThunkConfig Nothing)) p'
642 _ <- packThunk' True (ThunkPackConfig False (ThunkConfig Nothing)) p'
643 callProcessAndLogOutput (Notice, Error) $
643 callProcessAndLogOutput (Notice, Error) $
644− proc "cp" ["-r", "-T", p', p]
644+ proc cp ["-r", "-T", p', p]
645
645
646finalMsg :: Bool -> (a -> Text) -> Maybe (a -> Text)
646finalMsg :: Bool -> (a -> Text) -> Maybe (a -> Text)
647finalMsg noTrail s = if noTrail then Nothing else Just s
647finalMsg noTrail s = if noTrail then Nothing else Just s
@@ -682,7 +682,7 @@ unpackThunk' noTrail thunkDir = checkThunkDirectory "Can't pack/unpack from with
682
682
683 liftIO $ createDirectory obGitDir
683 liftIO $ createDirectory obGitDir
684 callProcessAndLogOutput (Notice, Error) $
684 callProcessAndLogOutput (Notice, Error) $
685− proc "cp" ["-r", "-T", thunkDir </> ".", obGitDir </> "orig-thunk"]
685+ proc cp ["-r", "-T", thunkDir </> ".", obGitDir </> "orig-thunk"]
686 callProcessAndLogOutput (Notice, Error) $
686 callProcessAndLogOutput (Notice, Error) $
687 proc "rm" ["-r", thunkDir]
687 proc "rm" ["-r", thunkDir]
688 callProcessAndLogOutput (Notice, Error) $
688 callProcessAndLogOutput (Notice, Error) $
lib/command/src/Obelisk/Command/Utils.hs+6−1
@@ -4,6 +4,7 @@
4{-# LANGUAGE ScopedTypeVariables #-}
4{-# LANGUAGE ScopedTypeVariables #-}
5{-# LANGUAGE OverloadedStrings #-}
5{-# LANGUAGE OverloadedStrings #-}
6{-# LANGUAGE PatternGuards #-}
6{-# LANGUAGE PatternGuards #-}
7+{-# LANGUAGE TemplateHaskell #-}
7module Obelisk.Command.Utils where
8module Obelisk.Command.Utils where
8
9
9import Control.Applicative hiding (many)
10import Control.Applicative hiding (many)
@@ -23,6 +24,7 @@ import Data.Void (Void)
23import System.Directory (canonicalizePath)
24import System.Directory (canonicalizePath)
24import System.Environment (getExecutablePath)
25import System.Environment (getExecutablePath)
25import System.Exit (ExitCode)
26import System.Exit (ExitCode)
27+import System.Which (staticWhich)
26import Text.Megaparsec as MP
28import Text.Megaparsec as MP
27import Text.Megaparsec.Char as MP
29import Text.Megaparsec.Char as MP
28
30
@@ -32,6 +34,9 @@ import Obelisk.CliApp
32getObeliskExe :: IO FilePath
34getObeliskExe :: IO FilePath
33getObeliskExe = getExecutablePath >>= canonicalizePath
35getObeliskExe = getExecutablePath >>= canonicalizePath
34
36
37+cp :: FilePath
38+cp = $(staticWhich "cp")
39+
35-- Check whether the working directory is clean
40-- Check whether the working directory is clean
36checkGitCleanStatus :: MonadObelisk m => FilePath -> Bool -> m Bool
41checkGitCleanStatus :: MonadObelisk m => FilePath -> Bool -> m Bool
37checkGitCleanStatus repo withIgnored = do
42checkGitCleanStatus repo withIgnored = do
@@ -83,7 +88,7 @@ isolateGitProc = setEnvOverride (overrides <>)
83-- | Recursively copy a directory using `cp -a` -- TODO: Should use -rT instead of -a
88-- | Recursively copy a directory using `cp -a` -- TODO: Should use -rT instead of -a
84copyDir :: FilePath -> FilePath -> ProcessSpec
89copyDir :: FilePath -> FilePath -> ProcessSpec
85copyDir src dest =
90copyDir src dest =
86− setCwd (Just src) $ proc "cp" ["-a", ".", dest] -- TODO: This will break if dest is relative since we change cwd
91+ setCwd (Just src) $ proc cp ["-a", ".", dest] -- TODO: This will break if dest is relative since we change cwd
87
92
88readGitProcess :: MonadObelisk m => FilePath -> [String] -> m Text
93readGitProcess :: MonadObelisk m => FilePath -> [String] -> m Text
89readGitProcess repo = readProcessAndLogOutput (Debug, Notice) . gitProc repo
94readGitProcess repo = readProcessAndLogOutput (Debug, Notice) . gitProc repo
lib/selftest/obelisk-selftest.cabal+4−2
@@ -11,16 +11,18 @@ library
11 base
11 base
12 , containers
12 , containers
13 , directory
13 , directory
14+ , filepath
14 , hspec
15 , hspec
15 , http-client
16 , http-client
16 , http-types
17 , http-types
17 , HUnit
18 , HUnit
19+ , network
18 , obelisk-cliapp
20 , obelisk-cliapp
19 , obelisk-executable-config-lookup
21 , obelisk-executable-config-lookup
20 , obelisk-run
22 , obelisk-run
23+ , which
21 , process
24 , process
22− , network
23− , shelly
25+ , shelly >= 1.9.0
24 , temporary
26 , temporary
25 , text
27 , text
26 exposed-modules:
28 exposed-modules:
lib/selftest/src/Obelisk/SelfTest.hs+73−33
@@ -4,6 +4,7 @@
4{-# LANGUAGE PatternGuards #-}
4{-# LANGUAGE PatternGuards #-}
5{-# LANGUAGE ScopedTypeVariables #-}
5{-# LANGUAGE ScopedTypeVariables #-}
6{-# LANGUAGE ViewPatterns #-}
6{-# LANGUAGE ViewPatterns #-}
7+{-# LANGUAGE TemplateHaskell #-}
7module Obelisk.SelfTest where
8module Obelisk.SelfTest where
8
9
9import Control.Exception (bracket, throw)
10import Control.Exception (bracket, throw)
@@ -11,7 +12,7 @@ import Control.Monad
11import Control.Monad.IO.Class
12import Control.Monad.IO.Class
12import Data.Bool (bool)
13import Data.Bool (bool)
13import Data.Function (fix)
14import Data.Function (fix)
14−import Data.Semigroup (Semigroup, (<>))
15+import Data.Semigroup ((<>))
15import qualified Data.Set as Set
16import qualified Data.Set as Set
16import Data.String
17import Data.String
17import Data.Text (Text)
18import Data.Text (Text)
@@ -21,10 +22,12 @@ import qualified Data.Text.IO as T
21import qualified Network.HTTP.Client as HTTP
22import qualified Network.HTTP.Client as HTTP
22import qualified Network.HTTP.Types as HTTP
23import qualified Network.HTTP.Types as HTTP
23import qualified Network.Socket as Socket
24import qualified Network.Socket as Socket
24−import Shelly
25+import Shelly hiding (cp)
25import System.Directory (withCurrentDirectory, getDirectoryContents)
26import System.Directory (withCurrentDirectory, getDirectoryContents)
26import System.Environment
27import System.Environment
27import System.Exit (ExitCode (..))
28import System.Exit (ExitCode (..))
29+import System.FilePath (replaceBaseName, takeBaseName)
30+import System.Which (staticWhich)
28import qualified System.Info
31import qualified System.Info
29import System.IO (Handle, hClose)
32import System.IO (Handle, hClose)
30import System.IO.Temp
33import System.IO.Temp
@@ -43,16 +46,21 @@ data ObRunState
43 | ObRunState_BackendStarted
46 | ObRunState_BackendStarted
44 deriving (Eq, Show)
47 deriving (Eq, Show)
45
48
46−doubleQuotes :: (IsString a, Semigroup a) => a -> a
49+cp :: FilePath
47−doubleQuotes s = "\"" <> s <> "\""
50+cp = $(staticWhich "cp")
51+
52+cabalPath :: FilePath
53+cabalPath = $(staticWhich "cabal")
54+
55+gitUserConfig :: [Text]
56+gitUserConfig = ["-c", "user.name=Obelisk Selftest", "-c", "user.email=noreply@example.com"]
48
57
49commit :: Text -> Sh ()
58commit :: Text -> Sh ()
50−commit msg = void $ run "git"
59+commit msg = void $ run "git" $ gitUserConfig <> [ "commit"
51− [ "commit"
52 , "--no-gpg-sign"
60 , "--no-gpg-sign"
53 , "--allow-empty"
61 , "--allow-empty"
54 , "-m"
62 , "-m"
55− , doubleQuotes msg
63+ , msg
56 ]
64 ]
57
65
58-- TODO replace Void and stop using loosely typed synchronous exceptions
66-- TODO replace Void and stop using loosely typed synchronous exceptions
@@ -68,6 +76,7 @@ tshow = T.pack . show
68shellyOb :: MonadIO m => (Sh a -> Sh a) -> Sh a -> m a
76shellyOb :: MonadIO m => (Sh a -> Sh a) -> Sh a -> m a
69shellyOb f obTest = shelly $ f obTest
77shellyOb f obTest = shelly $ f obTest
70
78
79+
71main :: IO ()
80main :: IO ()
72main = do
81main = do
73 -- Note: you can pass hspec arguments as well, eg: `-m <pattern>`
82 -- Note: you can pass hspec arguments as well, eg: `-m <pattern>`
@@ -82,33 +91,49 @@ main = do
82 withSystemTempDirectory "obelisk-impl-copy" $ \(fromString -> obeliskImpl) -> do
91 withSystemTempDirectory "obelisk-impl-copy" $ \(fromString -> obeliskImpl) -> do
83 void . shellyOb verbosity $ chdir obeliskImpl $ do
92 void . shellyOb verbosity $ chdir obeliskImpl $ do
84 user <- T.strip <$> run "whoami" []
93 user <- T.strip <$> run "whoami" []
85− run_ "cp" ["-rT", toTextIgnore obeliskImplRaw, toTextIgnore obeliskImpl]
94+ run_ cp ["-rT", toTextIgnore obeliskImplRaw, toTextIgnore obeliskImpl]
86 run_ "chown" ["-R", user, toTextIgnore obeliskImpl]
95 run_ "chown" ["-R", user, toTextIgnore obeliskImpl]
87 run_ "chmod" ["-R", "g-rw,o-rw", toTextIgnore obeliskImpl]
96 run_ "chmod" ["-R", "g-rw,o-rw", toTextIgnore obeliskImpl]
97+ f obeliskImpl
98+
99+ withInitCache f obeliskImpl =
100+ withSystemTempDirectory "init Cache λ" $ \initCache -> do
101+ -- Setup the ob init cache
102+ void . shellyOb verbosity $ chdir initCache $ do
103+ run_ "ob" ["init", "--symlink", toTextIgnore obeliskImpl]
104+ run_ "git" ["init"]
88
105
89− withSystemTempDirectory "init Cache λ" $ \initCache -> do
106+ f initCache
90− -- Setup the ob init cache
91− void . shellyOb verbosity $ chdir (fromString initCache) $ do
92− run_ "ob" ["init", "--symlink", toTextIgnore obeliskImpl]
93− run_ "git" ["init"]
94
107
95− f obeliskImpl initCache
108+ withObeliskImplAndInitCache f =
109+ withObeliskImpl $ \impl -> withInitCache (f impl) impl
96
110
97 httpManager <- HTTP.newManager HTTP.defaultManagerSettings
111 httpManager <- HTTP.newManager HTTP.defaultManagerSettings
98− [p0, p1, p2, p3] <- liftIO $ getFreePorts 4
99
112
100− withObeliskImpl $ \obeliskImpl initCache ->
113+ withObeliskImplAndInitCache $ \obeliskImpl initCache ->
101 hspec $ parallel $ do
114 hspec $ parallel $ do
102 let shelly_ = void . shellyOb verbosity
115 let shelly_ = void . shellyOb verbosity
103
116
104− inTmp :: (Shelly.FilePath -> Sh a) -> IO ()
117+ defaultTmpDirName = "test λ"
105− inTmp f = withTmp (chdir <*> f)
106
118
107− withTmp f = shelly_ . withSystemTempDirectory "test λ" $ f . fromString
119+ inTmp' :: FilePath -> (FilePath -> Sh a) -> IO ()
120+ inTmp' dirname f = withTmp' dirname (chdir <*> f)
121+ inTmp = inTmp' defaultTmpDirName
108
122
123+ withTmp' dirname f = shelly_ . withSystemTempDirectory dirname $ f . fromString
124+ withTmp = withTmp' defaultTmpDirName
125+
109− inTmpObInit f = inTmp $ \dir -> do
126+ inTmpObInit' dirname f = inTmp' dirname $ \dir -> do
110− run_ "cp" ["-rT", fromString initCache, toTextIgnore dir]
127+ run_ cp ["-rT", fromString initCache, toTextIgnore dir]
111 f dir
128 f dir
129+ inTmpObInit = inTmpObInit' defaultTmpDirName
130+
131+ -- To be used in tests that change the obelisk impl directory
132+ inTmpObInitWithImplCopy f = inTmpObInit $ \dir ->
133+ withObeliskImpl $ \(fromString -> implCopy) -> do
134+ run_ "rm" [thunk]
135+ run_ "ln" ["-s", implCopy, thunk]
136+ f dir
112
137
113 assertRevEQ a b = liftIO . assertEqual "" "" =<< diff a b
138 assertRevEQ a b = liftIO . assertEqual "" "" =<< diff a b
114 assertRevNE a b = liftIO . assertBool "" . (/= "") =<< diff a b
139 assertRevNE a b = liftIO . assertBool "" . (/= "") =<< diff a b
@@ -149,9 +174,12 @@ main = do
149
174
150 -- These tests fail with "Could not find module 'Obelisk.Generated.Static'"
175 -- These tests fail with "Could not find module 'Obelisk.Generated.Static'"
151 -- when not run by 'nix-build --attr selftest'
176 -- when not run by 'nix-build --attr selftest'
152− describe "ob run" $ parallel $ do
177+ describe "ob run" $ {- NOT parallel $ -} do
153− it "works in root directory" $ inTmpObInit $ \_ -> testObRunInDir p0 p1 Nothing httpManager
178+ it "works in root directory" $ inTmpObInit $ \_ -> testObRunInDir Nothing httpManager
154− it "works in sub directory" $ inTmpObInit $ \_ -> testObRunInDir p2 p3 (Just "frontend") httpManager
179+ it "works in sub directory" $ inTmpObInit $ \_ -> testObRunInDir (Just "frontend") httpManager
180+ it "works with differently named cabal files" $ inTmpObInit $ \_ -> do
181+ changeCabalPackageName "backend/backend.cabal" "new-backend"
182+ testObRunInDir Nothing httpManager
155
183
156 describe "obelisk project" $ parallel $ do
184 describe "obelisk project" $ parallel $ do
157 it "can build obelisk command" $ inTmpObInit $ \_ -> nixBuild ["-A", "command" , toTextIgnore obeliskImpl]
185 it "can build obelisk command" $ inTmpObInit $ \_ -> nixBuild ["-A", "command" , toTextIgnore obeliskImpl]
@@ -167,7 +195,7 @@ main = do
167 if System.Info.os == "darwin"
195 if System.Info.os == "darwin"
168 then it "can build ios" $ inTmpObInit $ \_ -> nixBuild ["-A", "ios.frontend"]
196 then it "can build ios" $ inTmpObInit $ \_ -> nixBuild ["-A", "ios.frontend"]
169 else it "can build android after accepting license" $ inTmpObInit $ \dir -> do
197 else it "can build android after accepting license" $ inTmpObInit $ \dir -> do
170− let defaultNixPath = dir </> ("default.nix" :: Shelly.FilePath)
198+ let defaultNixPath = dir </> ("default.nix" :: FilePath)
171 writefile defaultNixPath
199 writefile defaultNixPath
172 =<< T.replace
200 =<< T.replace
173 "# config.android_sdk.accept_license = false;"
201 "# config.android_sdk.accept_license = false;"
@@ -178,18 +206,21 @@ main = do
178 forM_ ["ghc", "ghcjs"] $ \compiler -> do
206 forM_ ["ghc", "ghcjs"] $ \compiler -> do
179 let
207 let
180 shellName = "shells." <> compiler
208 shellName = "shells." <> compiler
181− inShell cmd' = run "nix-shell" ["default.nix", "-A", fromString shellName, "--run", cmd']
209+ inShell cmd' = run_ "nix-shell" ["default.nix", "-A", fromString shellName, "--run", cmd']
182 it ("can enter " <> shellName) $ inTmpObInit $ \_ -> inShell "exit"
210 it ("can enter " <> shellName) $ inTmpObInit $ \_ -> inShell "exit"
211+ -- NOTE: We override the temporary directory name because cabal has a bug preventing new-build from working
212+ -- in a path that has unicode characters.
183− it ("can build in " <> shellName) $ inTmpObInit $ \_ -> inShell $ "cabal new-build --" <> fromString compiler <> " all"
213+ it ("can build in " <> shellName) $ inTmpObInit' "test" $ \_ -> inShell $
214+ T.pack cabalPath <> " --version; " <> T.pack cabalPath <> " new-build --" <> T.pack compiler <> " all"
184
215
185− it "has idempotent thunk update" $ inTmpObInit $ \_ -> do
216+ it "has idempotent thunk update" $ inTmpObInitWithImplCopy $ \_ -> do
186 _ <- pack
217 _ <- pack
187 u <- update
218 u <- update
188 uu <- update
219 uu <- update
189 assertRevEQ u uu
220 assertRevEQ u uu
190
221
191 describe "ob thunk pack/unpack" $ parallel $ do
222 describe "ob thunk pack/unpack" $ parallel $ do
192− it "has thunk pack and unpack inverses" $ inTmpObInit $ \_ -> do
223+ it "has thunk pack and unpack inverses" $ inTmpObInitWithImplCopy $ \_ -> do
193
224
194 _ <- pack
225 _ <- pack
195 e <- commitAll
226 e <- commitAll
@@ -249,8 +280,9 @@ main = do
249
280
250
281
251-- | Run `ob run` in the given directory (maximum of one level deep)
282-- | Run `ob run` in the given directory (maximum of one level deep)
252−testObRunInDir :: Socket.PortNumber -> Socket.PortNumber -> Maybe Shelly.FilePath -> HTTP.Manager -> Sh ()
283+testObRunInDir :: Maybe FilePath -> HTTP.Manager -> Sh ()
253−testObRunInDir p0 p1 mdir httpManager = handle_sh (\case ExitSuccess -> pure (); e -> throw e) $ do
284+testObRunInDir mdir httpManager = handle_sh (\case ExitSuccess -> pure (); e -> throw e) $ do
285+ [p0, p1] <- liftIO $ getFreePorts 2
254 let uri p = "http://localhost:" <> T.pack (show p) <> "/" -- trailing slash required for comparison
286 let uri p = "http://localhost:" <> T.pack (show p) <> "/" -- trailing slash required for comparison
255 writefile "config/common/route" $ uri p0
287 writefile "config/common/route" $ uri p0
256 maybe id chdir mdir $ runHandle "ob" ["run", "-v"] $ \stdout -> do
288 maybe id chdir mdir $ runHandle "ob" ["run", "-v"] $ \stdout -> do
@@ -264,7 +296,7 @@ testObRunInDir p0 p1 mdir httpManager = handle_sh (\case ExitSuccess -> pure ();
264 then errorExit $ "Reloading failed: expected " <> newUri <> " but got " <> runningUri
296 then errorExit $ "Reloading failed: expected " <> newUri <> " but got " <> runningUri
265 else exit 0
297 else exit 0
266
298
267−testThunkPack :: Shelly.FilePath -> Sh ()
299+testThunkPack :: FilePath -> Sh ()
268testThunkPack path' = withTempFile (T.unpack $ toTextIgnore path') "test-file" $ \file handle -> do
300testThunkPack path' = withTempFile (T.unpack $ toTextIgnore path') "test-file" $ \file handle -> do
269 let pack' = readProcessWithExitCode "ob" ["thunk", "pack", T.unpack $ toTextIgnore path'] ""
301 let pack' = readProcessWithExitCode "ob" ["thunk", "pack", T.unpack $ toTextIgnore path'] ""
270 ensureThunkPackFails q = liftIO $ pack' >>= \case
302 ensureThunkPackFails q = liftIO $ pack' >>= \case
@@ -285,7 +317,7 @@ testThunkPack path' = withTempFile (T.unpack $ toTextIgnore path') "test-file" $
285 liftIO $ T.hPutStrLn handle "test file" >> hClose handle
317 liftIO $ T.hPutStrLn handle "test file" >> hClose handle
286 ensureThunkPackFails "modified"
318 ensureThunkPackFails "modified"
287 -- Existing stashes
319 -- Existing stashes
288− void $ git ["stash"]
320+ void $ git $ gitUserConfig <> [ "stash" ]
289 ensureThunkPackFails "has stashes"
321 ensureThunkPackFails "has stashes"
290
322
291-- | Blocks until a non-empty line is available
323-- | Blocks until a non-empty line is available
@@ -330,6 +362,14 @@ obRunCheck httpManager _stdout frontendUri = do
330 req frontendUri >>= \r -> when (HTTP.responseStatus r /= HTTP.ok200) $ errorExit $
362 req frontendUri >>= \r -> when (HTTP.responseStatus r /= HTTP.ok200) $ errorExit $
331 "Request to frontend server failed: " <> T.pack (show r)
363 "Request to frontend server failed: " <> T.pack (show r)
332
364
365+-- | Rename a cabal file and do a really dumb, brittle search/replace in its content to update the name.
366+changeCabalPackageName :: FilePath -> Text -> Sh ()
367+changeCabalPackageName cabalFile newName = do
368+ contents <- readfile cabalFile
369+ writefile (replaceBaseName cabalFile (T.unpack newName)) $
370+ T.replace (" " <> T.pack (takeBaseName cabalFile)) (" " <> newName) contents -- WARNING: Super brittle
371+ rm cabalFile
372+
333getFreePorts :: Int -> IO [Socket.PortNumber]
373getFreePorts :: Int -> IO [Socket.PortNumber]
334getFreePorts 0 = pure []
374getFreePorts 0 = pure []
335getFreePorts n = Socket.withSocketsDo $ do
375getFreePorts n = Socket.withSocketsDo $ do
release.nix+8−95
@@ -1,97 +1,10 @@
1−{ self-args ? {
1+{
2− config.android_sdk.accept_license = true;
2+ supportedSystems ? [ "x86_64-linux" "x86_64-darwin" ]
3− iosSdkVersion = "10.2";
4− }
5−, local-self ? import ./. self-args
6}:
3}:
4+rec {
5+ recurseForDerivations = true;
6+ build = import ./all-builds.nix { inherit supportedSystems; };
7+ test = import ./all-tests.nix { inherit supportedSystems; };
7
8
8−let
9+ inherit (build) metaCache;
9− inherit (local-self.nixpkgs) lib runCommand nix;
10+}
10−
11− cacheBuildSystems = [ "x86_64-linux" "x86_64-darwin" ];
12−
13− obeliskPackagesCommon = [
14− "obelisk-frontend"
15− "obelisk-route"
16− "obelisk-executable-config-lookup"
17− ];
18−
19− obeliskPackagesBackend = obeliskPackagesCommon ++ [
20− "obelisk-asset-manifest"
21− "obelisk-asset-serve-snap"
22− "obelisk-backend"
23− "obelisk-cliapp"
24− "obelisk-command"
25− "obelisk-executable-config-inject"
26− "obelisk-frontend"
27− "obelisk-run"
28− "obelisk-route"
29− "obelisk-selftest"
30− "obelisk-snap-extras"
31− ];
32−
33− pnameToAttrs = pkgsSet: pnames:
34− lib.listToAttrs (map
35− (name: { inherit name; value = pkgsSet.${name}; })
36− pnames);
37−
38− concatDepends = let
39− extractDeps = x: (x.override {
40− mkDerivation = drv: {
41− out = builtins.concatLists [
42− (drv.buildDepends or [])
43− (drv.libraryHaskellDepends or [])
44− (drv.executableHaskellDepends or [])
45− ];
46− };
47− }).out;
48− in pkgAttrs: builtins.concatLists (map extractDeps (builtins.attrValues pkgAttrs));
49−
50− perPlatform = lib.genAttrs cacheBuildSystems (system: let
51− obelisk = import ./. (self-args // { inherit system; });
52− reflex-platform = obelisk.reflex-platform;
53− ghc = pnameToAttrs
54− obelisk.haskellPackageSets.ghc
55− obeliskPackagesBackend;
56− ghcjs = pnameToAttrs
57− obelisk.haskellPackageSets.ghcjs
58− obeliskPackagesCommon;
59− cachePackages = builtins.concatLists [
60− (builtins.attrValues ghc)
61− (builtins.attrValues ghcjs)
62− (concatDepends ghc)
63− (concatDepends ghcjs)
64− (lib.optional reflex-platform.androidSupport androidSkeleton)
65− (lib.optional reflex-platform.iosSupport iosSkeleton)
66− [ command serverSkeletonExe serverSkeletonShell ]
67− ];
68− command = obelisk.command;
69− skeleton = import ./skeleton { inherit obelisk; };
70− serverSkeletonExe = skeleton.exe;
71− # TODO fix nixpkgs so it doesn't try to run the result of haskell shells as setup hooks.
72− serverSkeletonShell = local-self.nixpkgs.runCommand "shell-safe-for-dep" {} ''
73− touch "$out"
74− echo "return" >> "$out"
75− cat "${skeleton.shells.ghc}" >> "$out"
76− '';
77− androidSkeleton = (import ./skeleton { inherit obelisk; }).android.frontend;
78− iosSkeleton = (import ./skeleton { inherit obelisk; }).ios.frontend;
79− in {
80− inherit
81− command
82− ghc ghcjs
83− serverSkeletonExe
84− serverSkeletonShell
85− ;
86− cache = reflex-platform.pinBuildInputs "obelisk-${system}" cachePackages;
87− } // lib.optionalAttrs reflex-platform.androidSupport {
88− inherit androidSkeleton;
89− } // lib.optionalAttrs reflex-platform.iosSupport {
90− inherit iosSkeleton;
91− });
92−
93− metaCache = local-self.reflex-platform.pinBuildInputs
94− "obelisk-everywhere"
95− (map (a: a.cache) (builtins.attrValues perPlatform));
96−
97−in perPlatform // { inherit metaCache; }
skeleton/frontend/frontend.cabal+1
@@ -9,6 +9,7 @@ library
9 , common
9 , common
10 , obelisk-frontend
10 , obelisk-frontend
11 , obelisk-route
11 , obelisk-route
12+ , jsaddle
12 , reflex-dom
13 , reflex-dom
13 , obelisk-executable-config-lookup
14 , obelisk-executable-config-lookup
14 , obelisk-generated-static
15 , obelisk-generated-static
skeleton/frontend/src/Frontend.hs+12
@@ -4,8 +4,10 @@
4
4
5module Frontend where
5module Frontend where
6
6
7+import Control.Monad
7import qualified Data.Text as T
8import qualified Data.Text as T
8import qualified Data.Text.Encoding as T
9import qualified Data.Text.Encoding as T
10+import Language.Javascript.JSaddle (eval, liftJSM)
9
11
10import Obelisk.Frontend
12import Obelisk.Frontend
11import Obelisk.Configs
13import Obelisk.Configs
@@ -18,6 +20,9 @@ import Common.Api
18import Common.Route
20import Common.Route
19
21
20
22
23+-- This runs in a monad that can be run on the client or the server.
24+-- To run code in a pure client or pure server context, use one of the
25+-- `prerender` functions.
21frontend :: Frontend (R FrontendRoute)
26frontend :: Frontend (R FrontendRoute)
22frontend = Frontend
27frontend = Frontend
23 { _frontend_head = do
28 { _frontend_head = do
@@ -26,6 +31,13 @@ frontend = Frontend
26 , _frontend_body = do
31 , _frontend_body = do
27 el "h1" $ text "Welcome to Obelisk!"
32 el "h1" $ text "Welcome to Obelisk!"
28 el "p" $ text $ T.pack commonStuff
33 el "p" $ text $ T.pack commonStuff
34+
35+ -- `prerender` and `prerender_` let you choose a widget to run on the server
36+ -- during prerendering and a different widget to run on the client with
37+ -- JavaScript. The following will generate a `blank` widget on the server and
38+ -- print "Hello, World!" on the client.
39+ prerender_ blank $ liftJSM $ void $ eval ("console.log('Hello, World!')" :: T.Text)
40+
29 elAttr "img" ("src" =: static @"obelisk.jpg") blank
41 elAttr "img" ("src" =: static @"obelisk.jpg") blank
30 el "div" $ do
42 el "div" $ do
31 exampleConfig <- getConfig "common/example"
43 exampleConfig <- getConfig "common/example"