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