on old-reflex-platform help sign in
obelisk/default.nix
332 lines 15 kB history raw
1{ system ? builtins.currentSystem
2, profiling ? false
3, iosSdkVersion ? "10.2"
4}:
5let
6 getReflexPlatform = sys: import ./dep/reflex-platform { inherit iosSdkVersion; system = sys; };
7 reflex-platform = getReflexPlatform system;
8 inherit (reflex-platform) hackGet nixpkgs;
9 pkgs = nixpkgs;
10in with pkgs.haskell.lib; with pkgs.lib;
11let
12 # TODO: Remove this after updating nixpkgs: https://github.com/NixOS/nixpkgs/issues/37750
13 justStaticExecutables' = drv: let
14 drv' = justStaticExecutables drv;
15 in if pkgs.stdenv.isDarwin
16 then removeConfigureFlag drv' "--ghc-option=-optl=-dead_strip"
17 else drv';
19 commandRuntimeDeps = pkgs: with pkgs; [
20 coreutils
21 git
22 gitAndTools.hub
23 nix-prefetch-git
24 nixStable
25 openssh
26 ];
28 #TODO: Upstream
29 # Modify a Haskell package to add completion scripts for the given
30 # executable produced by it. These completion scripts will be picked up
31 # automatically if the resulting derivation is installed, e.g. by
32 # `nix-env -i`.
33 addOptparseApplicativeCompletionScripts = exeName: pkg: overrideCabal pkg (drv: {
34 postInstall = (drv.postInstall or "") + ''
35 BASH_COMP_DIR="$out/share/bash-completion/completions"
36 mkdir -p "$BASH_COMP_DIR"
37 "$out/bin/${exeName}" --bash-completion-script "$out/bin/${exeName}" >"$BASH_COMP_DIR/ob"
39 ZSH_COMP_DIR="$out/share/zsh/vendor-completions"
40 mkdir -p "$ZSH_COMP_DIR"
41 "$out/bin/${exeName}" --zsh-completion-script "$out/bin/${exeName}" >"$ZSH_COMP_DIR/_ob"
43 FISH_COMP_DIR="$out/share/fish/vendor_completions.d"
44 mkdir -p "$FISH_COMP_DIR"
45 "$out/bin/${exeName}" --fish-completion-script "$out/bin/${exeName}" >"$FISH_COMP_DIR/ob.fish"
46 '';
47 });
49 # The haskell environment used to build Obelisk itself, e.g. the 'ob' command
50 ghcObelisk = reflex-platform.ghc.override {
51 overrides = composeExtensions defaultHaskellOverrides (self: super: {
52 mkDerivation = args: super.mkDerivation (args // {
53 enableLibraryProfiling = profiling;
54 });
56 # Dynamic linking with split objects dramatically increases startup time (about 0.5 seconds on a decent machine with SSD)
57 obelisk-command = addOptparseApplicativeCompletionScripts "ob" (justStaticExecutables' super.obelisk-command);
59 optparse-applicative = self.callHackage "optparse-applicative" "0.14.0.0" {};
61 Cabal = self.callHackage "Cabal" "2.0.0.2" {};
62 });
63 };
65 fixUpstreamPkgs = self: super: {
66 megaparsec = self.callHackage "megaparsec" "6.0.0" {};
67 modern-uri = pkgs.haskell.lib.dontCheck super.modern-uri;
68 github = pkgs.haskell.lib.doJailbreak (self.callCabal2nix "github" (pkgs.fetchFromGitHub {
69 owner = "phadej";
70 repo = "github";
71 rev = "v0.19";
72 sha256 = "0gzqhpvgab9j0v5647m65d3jjy8kdlwaqyr5pc2hjyc0w8gvgcak";
73 }) {});
75 algebraic-graphs = pkgs.haskell.lib.dontCheck (pkgs.haskell.lib.doJailbreak
76 (self.callCabal2nix "algebraic-graphs" (pkgs.fetchFromGitHub {
77 owner = "snowleopard";
78 repo = "alga";
79 rev = "480a73137e9b38ad3f1bc2c628847953d2fb3e25";
80 sha256 = "0dpwi5ffs88brl3lz51bwb004c6zm8ds8pkw1vzsg2a6aaiyhlzl";
81 }) {}));
83 monoidal-containers =
84 let src = pkgs.fetchFromGitHub {
85 owner = "obsidiansystems";
86 repo = "monoidal-containers";
87 rev = "af5f6cedd1acd8725b19fd6a0277f83906603491";
88 sha256 = "11v20ing8lrb5ccf6g9iihwcw5d22yj2ifw15v04ypn19y8kariw";
89 };
90 in pkgs.haskell.lib.dontCheck (self.callCabal2nix "monoidal-containers" src {});
92 # Need deriveSomeUniverse
93 # PR: https://github.com/dmwit/universe/pull/32
94 universe-template = self.callCabal2nix "universe-template" (pkgs.fetchFromGitHub {
95 owner = "obsidiansystems";
96 repo = "universe";
97 rev = "5a2fc823caa4163411d7e41aa80e67cefb15944a";
98 sha256 = "0ll2z0fh18z6x8jl8kbp7ldagwccz3wjmvrw1gw752z058n82yfa";
99 } + /template) {};
101 # Need ShowTag, EqTag, and OrdTag instances
102 dependent-sum-template = self.callCabal2nix "dependent-sum-template" (pkgs.fetchFromGitHub {
103 owner = "mokus0";
104 repo = "dependent-sum-template";
105 rev = "bfe9c37f4eaffd8b17c03f216c06a0bfb66f7df7";
106 sha256 = "1w3s7nvw0iw5li3ry7s8r4651qwgd22hmgz6by0iw3rm64fy8x0y";
107 }) {};
108 };
110 cleanSource = builtins.filterSource (name: _: let baseName = builtins.baseNameOf name; in !(
111 builtins.match "^\\.ghc\\.environment.*" baseName != null ||
112 baseName == "cabal.project.local"
113 ));
115 executableConfig = import ./lib/executable-config { nixpkgs = pkgs; filterGitSource = cleanSource; };
117 addLibs = self: super: {
118 obelisk-asset-manifest = self.callCabal2nix "obelisk-asset-manifest" (hackGet ./lib/asset + "/manifest") {};
119 obelisk-asset-serve-snap = self.callCabal2nix "obelisk-asset-serve-snap" (hackGet ./lib/asset + "/serve-snap") {};
120 obelisk-backend = self.callCabal2nix "obelisk-backend" (cleanSource ./lib/backend) {};
121 obelisk-cliapp = self.callCabal2nix "obelisk-cliapp" (cleanSource ./lib/cliapp) {};
122 obelisk-command = (self.callCabal2nix "obelisk-command" (cleanSource ./lib/command) {});
123 obelisk-executable-config = executableConfig.haskellPackage self;
124 obelisk-executable-config-inject = executableConfig.platforms.web.inject self; # TODO handle platforms.{ios,android}
125 obelisk-frontend = self.callCabal2nix "obelisk-frontend" (cleanSource ./lib/frontend) {};
126 obelisk-migration = self.callCabal2nix "obelisk-migration" (cleanSource ./lib/migration) {};
127 obelisk-run = self.callCabal2nix "obelisk-run" (cleanSource ./lib/run) {};
128 obelisk-route = self.callCabal2nix "obelisk-route" (cleanSource ./lib/route) {};
129 obelisk-selftest = self.callCabal2nix "obelisk-selftest" (cleanSource ./lib/selftest) {};
130 obelisk-snap = self.callCabal2nix "obelisk-snap" (cleanSource ./lib/snap) {};
131 obelisk-snap-extras = self.callCabal2nix "obelisk-snap-extras" (cleanSource ./lib/snap-extras) {};
132 };
134 inherit (import ./lib/asset/assets.nix { inherit nixpkgs; }) mkAssets;
136 defaultHaskellOverrides = composeExtensions fixUpstreamPkgs addLibs;
137in
138with pkgs.lib;
139rec {
140 inherit reflex-platform;
141 inherit (reflex-platform) nixpkgs pinBuildInputs;
142 pathGit = ./.; # Used in CI by the migration graph hash algorithm to correctly ignore files.
143 path = reflex-platform.filterGit ./.;
144 obelisk = ghcObelisk;
145 commandWithMigration = ghcObelisk.obelisk-command.overrideAttrs (drv: {
146 postInstall = (drv.postInstall or "") +
147 ''cp -r ${./migration} $out/migration;'';
148 });
149 command = pkgs.runCommand commandWithMigration.name { nativeBuildInputs = [pkgs.makeWrapper]; } ''
150 mkdir -p "$out/bin"
151 ln -s '${commandWithMigration}/bin/ob' "$out/bin/ob"
152 wrapProgram "$out"/bin/ob --prefix PATH : ${pkgs.lib.makeBinPath (commandRuntimeDeps pkgs)}
153 '';
154 shell = pinBuildInputs "obelisk-shell" ([command] ++ commandRuntimeDeps pkgs) [];
156 selftest = pkgs.writeScript "selftest" ''
157 #!/usr/bin/env bash
158 set -euo pipefail
160 PATH="${command}/bin:$PATH"
161 export OBELISK_IMPL="${hackGet ./.}"
162 "${justStaticExecutables' ghcObelisk.obelisk-selftest}/bin/obelisk-selftest" "$@"
163 '';
164 #TODO: Why can't I build ./skeleton directly as a derivation? `nix-build -E ./.` doesn't work
165 skeleton = pkgs.runCommand "skeleton" {
166 dir = builtins.filterSource (path: type: builtins.trace path (baseNameOf path != ".obelisk")) ./skeleton;
167 } ''
168 ln -s "$dir" "$out"
169 '';
170 nullIfAbsent = p: if pathExists p then p else null;
171 haskellOverrides = addLibs;
172 #TODO: Avoid copying files within the nix store. Right now, obelisk-asset-manifest-generate copies files into a big blob so that the android/ios static assets can be imported from there; instead, we should get everything lined up right before turning it into an APK, so that copies, if necessary, only exist temporarily.
173 processAssets = { src, packageName ? "static", moduleName ? "Static" }: pkgs.runCommand "asset-manifest" {
174 inherit src;
175 outputs = [ "out" "haskellManifest" "symlinked" ];
176 buildInputs = [
177 (reflex-platform.ghc.callCabal2nix "obelisk-asset-manifest" (hackGet ./lib/asset + "/manifest") {})
178 ];
179 } ''
180 set -euo pipefail
181 touch "$out"
182 obelisk-asset-manifest-generate "$src" "$haskellManifest" ${packageName} ${moduleName} "$symlinked"
183 '';
185 compressedJs = frontend: pkgs.runCommand "compressedJs" { buildInputs = [ pkgs.closurecompiler ]; } ''
186 mkdir $out
187 cd $out
188 ln -s "${justStaticExecutables frontend}/bin/frontend.jsexe/all.js" all.unminified.js
189 closure-compiler --externs "${reflex-platform.ghcjsExternsJs}" -O ADVANCED --jscomp_warning=checkVars --create_source_map="all.js.map" --source_map_format=V3 --js_output_file="all.js" all.unminified.js
190 echo "//# sourceMappingURL=all.js.map" >> all.js
191 '';
193 serverExe = backend: frontend: assets: config:
194 pkgs.runCommand "serverExe" {} ''
195 mkdir $out
196 set -eux
197 ln -s "${justStaticExecutables backend}"/bin/backend $out/backend
198 ln -s "${mkAssets assets}" $out/static.assets
199 cp -r ${config} $out/config
200 ln -s ${mkAssets (compressedJs frontend)} $out/frontend.jsexe.assets
201 '';
203 server = { exe, hostName, adminEmail, routeHost, enableHttps, ... }:
204 let system = "x86_64-linux";
205 nixos = import (pkgs.path + /nixos);
206 backendPort = 8000;
207 in nixos {
208 inherit system;
209 configuration = args: {
210 imports = [
211 (pkgs.path + /nixos/modules/virtualisation/amazon-image.nix)
212 ];
213 networking = {
214 inherit hostName;
215 firewall.allowedTCPPorts = if enableHttps then [ 80 443 ] else [ 80 ];
216 };
217 services.nginx = {
218 enable = true;
219 virtualHosts."${routeHost}" = {
220 enableACME = enableHttps;
221 forceSSL = enableHttps;
222 locations."/" = {
223 proxyPass = "http://127.0.0.1:" + toString backendPort;
224 };
225 };
226 };
227 security.acme.certs = if enableHttps then {
228 "${routeHost}".email = adminEmail;
229 } else { };
230 systemd.services.backend = {
231 wantedBy = [ "multi-user.target" ];
232 after = [ "network.target" ];
233 restartIfChanged = true;
234 script = ''
235 ln -sft . "${exe}"/*
236 mkdir -p log
237 exec ./backend >>backend.out 2>>backend.err </dev/null
238 '';
239 serviceConfig = {
240 User = "backend";
241 KillMode = "process";
242 WorkingDirectory = "~";
243 Restart = "always";
244 RestartSec = 5;
245 };
246 };
247 users.extraUsers.backend = {
248 description = "backend service";
249 home = "/var/lib/backend";
250 createHome = true;
251 isSystemUser = true;
252 group = "backend";
253 };
254 ec2.hvm = true;
255 };
256 };
259 # An Obelisk project is a reflex-platform project with a predefined layout and role for each component
260 project = base: projectDefinition:
261 let configPath = base + "/config";
262 static = base + "/static";
263 processedStatic = processAssets { src = static; };
264 projectOut = sys: (getReflexPlatform sys).project (args@{ nixpkgs, ... }:
265 let mkProject = { android ? null #TODO: Better error when missing
266 , ios ? null #TODO: Better error when missing
267 , packages ? {}
268 , overrides ? _: _: {}
269 }:
270 let frontendName = "frontend";
271 backendName = "backend";
272 commonName = "common";
273 staticName = "static";
274 staticPath = base + "/static";
275 # The packages whose names and roles are defined by this package
276 predefinedPackages = filterAttrs (_: x: x != null) {
277 ${frontendName} = nullIfAbsent (base + "/frontend");
278 ${commonName} = nullIfAbsent (base + "/common");
279 ${backendName} = nullIfAbsent (base + "/backend");
280 };
281 combinedPackages = predefinedPackages // packages;
282 projectOverrides = self: super: {
283 ${staticName} = dontHaddock (self.callCabal2nix "static" processedStatic.haskellManifest {});
284 ${backendName} = addBuildDepend super.${backendName} self.obelisk-run;
285 };
286 totalOverrides = composeExtensions (composeExtensions defaultHaskellOverrides projectOverrides) overrides;
287 in {
288 overrides = totalOverrides;
289 packages = combinedPackages;
290 shells = {
291 ghc = (filter (x: hasAttr x combinedPackages) [
292 backendName
293 commonName
294 frontendName
295 ]);
296 ghcjs = filter (x: hasAttr x combinedPackages) [
297 frontendName
298 commonName
299 ];
300 };
301 withHoogle = false; # Setting this to `true` makes shell reloading far slower
302 android = {
303 ${if android == null then null else frontendName} = {
304 executableName = "frontend";
305 ${if builtins.pathExists staticPath then "assets" else null} = processedStatic.symlinked;
306 } // android;
307 };
308 ios = {
309 ${if ios == null then null else frontendName} = {
310 executableName = "frontend";
311 ${if builtins.pathExists staticPath then "staticSrc" else null} = processedStatic.symlinked;
312 } // ios;
313 };
314 };
315 in mkProject (projectDefinition args));
316 serverOn = sys: config: serverExe (projectOut sys).ghc.backend (projectOut system).ghcjs.frontend static config;
317 # `exe` is project's backend executable, with frontend assets, config, etc.
318 # `linuxExe` is the same but built for x86_64-linux.
319 exe = serverOn system configPath;
320 linuxExe = serverOn "x86_64-linux" configPath;
321 in projectOut system // {
322 inherit exe linuxExe;
323 server = args@{ hostName, adminEmail, routeHost, enableHttps, config }:
324 server (args // { exe = serverOn "x86_64-linux" config;});
325 obelisk = import (base + "/.obelisk/impl") {};
326 };
327 haskellPackageSets = {
328 ghc = reflex-platform.ghc.override {
329 overrides = defaultHaskellOverrides;
330 };
331 };