on kassandra-test help sign in
obelisk/default.nix
364 lines 15 kB history raw
1{ system ? builtins.currentSystem
2, profiling ? false
3, iosSdkVersion ? "10.2"
4, config ? {}
5, reflex-platform-func ? import ./dep/reflex-platform
6}:
7let
8 reflex-platform = getReflexPlatform { inherit system; };
9 inherit (reflex-platform) hackGet nixpkgs;
10 pkgs = nixpkgs;
12 inherit (import dep/gitignore.nix { inherit (nixpkgs) lib; }) gitignoreSource;
14 getReflexPlatform = { system, enableLibraryProfiling ? profiling }: reflex-platform-func {
15 inherit iosSdkVersion config system enableLibraryProfiling;
17 nixpkgsOverlays = [
18 (import ./nixpkgs-overlays)
19 ];
21 haskellOverlays = [
22 (import ./haskell-overlays/misc-deps.nix { inherit hackGet; })
23 pkgs.obeliskExecutableConfig.haskellOverlay
24 (import ./haskell-overlays/obelisk.nix)
25 (import ./haskell-overlays/tighten-ob-exes.nix)
26 ];
27 };
29 # The haskell environment used to build Obelisk itself, e.g. the 'ob' command
30 ghcObelisk = reflex-platform.ghc;
32 # Development environments for obelisk packages.
33 ghcObeliskEnvs = pkgs.lib.mapAttrs (n: v: reflex-platform.workOn ghcObelisk v) ghcObelisk;
35 inherit (import ./lib/asset/assets.nix { inherit nixpkgs; }) mkAssets;
37 haskellLib = pkgs.haskell.lib;
39in rec {
40 inherit reflex-platform;
41 inherit (reflex-platform) nixpkgs pinBuildInputs;
42 inherit (nixpkgs) lib;
43 pathGit = ./.; # Used in CI by the migration graph hash algorithm to correctly ignore files.
44 path = reflex-platform.filterGit ./.;
45 obelisk = ghcObelisk;
46 obeliskEnvs = pkgs.lib.filterAttrs (k: _: pkgs.lib.strings.hasPrefix "obelisk-" k) ghcObeliskEnvs;
47 command = ghcObelisk.obelisk-command;
48 shell = pinBuildInputs "obelisk-shell" ([command] ++ command.commandRuntimeDeps);
50 selftest = pkgs.writeScript "selftest" ''
51 #!${pkgs.runtimeShell}
52 set -euo pipefail
54 PATH="${command}/bin:$PATH"
55 cd ${./.}
56 "${ghcObelisk.obelisk-selftest}/bin/obelisk-selftest" +RTS -N -RTS "$@"
57 '';
58 skeleton = pkgs.runCommand "skeleton" {
59 dir = builtins.filterSource (path: type: builtins.trace path (baseNameOf path != ".obelisk")) ./skeleton;
60 } ''
61 ln -s "$dir" "$out"
62 '';
63 nullIfAbsent = p: if lib.pathExists p then p else null;
64 #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.
65 processAssets = { src, packageName ? "obelisk-generated-static", moduleName ? "Obelisk.Generated.Static" }: pkgs.runCommand "asset-manifest" {
66 inherit src;
67 outputs = [ "out" "haskellManifest" "symlinked" ];
68 nativeBuildInputs = [ ghcObelisk.obelisk-asset-manifest ];
69 } ''
70 set -euo pipefail
71 touch "$out"
72 mkdir -p "$symlinked"
73 obelisk-asset-manifest-generate "$src" "$haskellManifest" ${packageName} ${moduleName} "$symlinked"
74 '';
76 compressedJs = frontend: optimizationLevel: pkgs.runCommand "compressedJs" {} ''
77 mkdir $out
78 cd $out
79 # TODO profiling + static shouldn't break and need an ad-hoc workaround like that
80 ln -s "${haskellLib.justStaticExecutables frontend}/bin/frontend.jsexe/all.js" all.unminified.js
81 ${if optimizationLevel == null then ''
82 ln -s all.unminified.js all.js
83 '' else ''
84 ${pkgs.closurecompiler}/bin/closure-compiler --externs "${reflex-platform.ghcjsExternsJs}" -O ${optimizationLevel} --jscomp_warning=checkVars --create_source_map="all.js.map" --source_map_format=V3 --js_output_file="all.js" all.unminified.js
85 echo "//# sourceMappingURL=all.js.map" >> all.js
86 ''}
87 '';
89 serverModules = {
90 mkBaseEc2 = { nixosPkgs, ... }: {...}: {
91 imports = [
92 (nixosPkgs.path + /nixos/modules/virtualisation/amazon-image.nix)
93 ];
94 ec2.hvm = true;
95 };
97 mkDefaultNetworking = { adminEmail, enableHttps, hostName, routeHost, ... }: {...}: {
98 networking = {
99 inherit hostName;
100 firewall.allowedTCPPorts = if enableHttps then [ 80 443 ] else [ 80 ];
101 };
103 # `amazon-image.nix` already sets these but if the user provides their own module then
104 # forgetting these can cause them to lose access to the server!
105 # https://github.com/NixOS/nixpkgs/blob/fab05f17d15e4e125def4fd4e708d205b41d8d74/nixos/modules/virtualisation/amazon-image.nix#L133-L136
106 services.openssh.enable = true;
107 services.openssh.permitRootLogin = "prohibit-password";
109 security.acme.certs = if enableHttps then {
110 "${routeHost}".email = adminEmail;
111 } else {};
112 };
114 mkObeliskApp =
115 { exe
116 , routeHost
117 , enableHttps
118 , name ? "backend"
119 , user ? name
120 , group ? user
121 , baseUrl ? "/"
122 , internalPort ? 8000
123 , backendArgs ? "--port=${toString internalPort}"
124 , ...
125 }: {...}: {
126 services.nginx = {
127 enable = true;
128 virtualHosts."${routeHost}" = {
129 enableACME = enableHttps;
130 forceSSL = enableHttps;
131 locations.${baseUrl} = {
132 proxyPass = "http://127.0.0.1:" + toString internalPort;
133 proxyWebsockets = true;
134 };
135 };
136 };
137 systemd.services.${name} = {
138 wantedBy = [ "multi-user.target" ];
139 after = [ "network.target" ];
140 restartIfChanged = true;
141 path = [ pkgs.gnutar ];
142 script = ''
143 ln -sft . '${exe}'/*
144 mkdir -p log
145 exec ./backend ${backendArgs} </dev/null
146 '';
147 serviceConfig = {
148 User = user;
149 KillMode = "process";
150 WorkingDirectory = "~";
151 Restart = "always";
152 RestartSec = 5;
153 };
154 };
155 users = {
156 users.${user} = {
157 description = "${user} service";
158 home = "/var/lib/${user}";
159 createHome = true;
160 isSystemUser = true;
161 group = group;
162 };
163 groups.${group} = {};
164 };
165 };
166 };
168 inherit mkAssets;
170 serverExe = backend: frontend: assets: optimizationLevel: version:
171 pkgs.runCommand "serverExe" {} ''
172 mkdir $out
173 set -eux
174 ln -s "${if profiling then backend else haskellLib.justStaticExecutables backend}"/bin/* $out/
175 ln -s "${mkAssets assets}" $out/static.assets
176 ln -s ${mkAssets (compressedJs frontend optimizationLevel)} $out/frontend.jsexe.assets
177 echo ${version} > $out/version
178 '';
180 server = { exe, hostName, adminEmail, routeHost, enableHttps, version, module ? serverModules.mkBaseEc2 }@args:
181 let
182 nixos = import (pkgs.path + /nixos);
183 in nixos {
184 system = "x86_64-linux";
185 configuration = {
186 imports = [
187 (module { inherit exe hostName adminEmail routeHost enableHttps version; nixosPkgs = pkgs; })
188 (serverModules.mkDefaultNetworking args)
189 (serverModules.mkObeliskApp args)
190 ];
191 };
192 };
194 # An Obelisk project is a reflex-platform project with a predefined layout and role for each component
195 project = base': projectDefinition:
196 let
197 projectOut = { system, enableLibraryProfiling ? profiling }: let reflexPlatformProject = (getReflexPlatform { inherit system enableLibraryProfiling; }).project; in reflexPlatformProject (args@{ nixpkgs, ... }:
198 let
199 inherit (lib.strings) hasPrefix;
200 mkProject =
201 { android ? null #TODO: Better error when missing
202 , ios ? null #TODO: Better error when missing
203 , packages ? {}
204 , overrides ? _: _: {}
205 , staticFiles ? null
206 , tools ? _: []
207 , shellToolOverrides ? _: _: {}
208 , withHoogle ? false # Setting this to `true` makes shell reloading far slower
209 , __closureCompilerOptimizationLevel ? "ADVANCED" # Set this to `null` to skip the closure-compiler step
210 , __withGhcide ? false
211 }:
212 let
213 allConfig = nixpkgs.lib.makeExtensible (self: {
214 base = base';
215 inherit args;
216 userSettings = {
217 inherit android ios packages overrides tools shellToolOverrides withHoogle __closureCompilerOptimizationLevel __withGhcide;
218 staticFiles = if staticFiles == null then self.base + /static else staticFiles;
219 };
220 frontendName = "frontend";
221 backendName = "backend";
222 commonName = "common";
223 staticName = "obelisk-generated-static";
224 staticFilesImpure = let fs = self.userSettings.staticFiles; in if lib.isDerivation fs then fs else toString fs;
225 processedStatic = processAssets { src = self.userSettings.staticFiles; };
226 # The packages whose names and roles are defined by this package
227 predefinedPackages = lib.filterAttrs (_: x: x != null) {
228 ${self.frontendName} = nullIfAbsent (self.base + "/frontend");
229 ${self.commonName} = nullIfAbsent (self.base + "/common");
230 ${self.backendName} = nullIfAbsent (self.base + "/backend");
231 };
232 shellPackages = {};
233 combinedPackages = self.predefinedPackages // self.userSettings.packages // self.shellPackages;
234 projectOverrides = self': super': {
235 ${self.staticName} = haskellLib.dontHaddock (self'.callCabal2nix self.staticName self.processedStatic.haskellManifest {});
236 ${self.backendName} = haskellLib.addBuildDepend super'.${self.backendName} self'.obelisk-run;
237 };
238 totalOverrides = lib.composeExtensions self.projectOverrides self.userSettings.overrides;
239 privateConfigDirs = ["config/backend"];
240 injectableConfig = builtins.filterSource (path: _:
241 !(lib.lists.any (x: hasPrefix (toString self.base + "/" + toString x) (toString path)) self.privateConfigDirs)
242 );
243 __androidWithConfig = configPath: {
244 ${if self.userSettings.android == null then null else self.frontendName} = {
245 executableName = "frontend";
246 ${if builtins.pathExists self.userSettings.staticFiles then "assets" else null} =
247 nixpkgs.obeliskExecutableConfig.platforms.android.inject
248 (self.injectableConfig configPath)
249 self.processedStatic.symlinked;
250 } // self.userSettings.android;
251 };
252 __iosWithConfig = configPath: {
253 ${if self.userSettings.ios == null then null else self.frontendName} = {
254 executableName = "frontend";
255 ${if builtins.pathExists self.userSettings.staticFiles then "staticSrc" else null} =
256 nixpkgs.obeliskExecutableConfig.platforms.ios.inject
257 (self.injectableConfig configPath)
258 self.processedStatic.symlinked;
259 } // self.userSettings.ios;
260 };
262 shells-ghc = builtins.attrNames (self.predefinedPackages // self.shellPackages);
264 shells-ghcjs = [
265 self.frontendName
266 self.commonName
267 ];
269 shells-ghcSavedSplices = [
270 self.commonName
271 self.frontendName
272 ];
274 shellToolOverrides = lib.composeExtensions
275 self.userSettings.shellToolOverrides
276 (if self.userSettings.__withGhcide
277 then (import ./haskell-overlays/ghcide.nix)
278 else (_: _: {})
279 );
281 project = reflexPlatformProject ({...}: self.projectConfig);
282 projectConfig = {
283 inherit (self) shellToolOverrides;
284 inherit (self.userSettings) tools withHoogle;
285 overrides = self.totalOverrides;
286 packages = self.combinedPackages;
287 shells = {
288 ${if self.userSettings.android == null && self.userSettings.ios == null then null else "ghcSavedSplices"} =
289 lib.filter (x: lib.hasAttr x self.combinedPackages) self.shells-ghcSavedSplices;
290 ghc = lib.filter (x: lib.hasAttr x self.combinedPackages) self.shells-ghc;
291 ghcjs = lib.filter (x: lib.hasAttr x self.combinedPackages) self.shells-ghcjs;
292 };
293 android = self.__androidWithConfig (self.base + "/config");
294 ios = self.__iosWithConfig (self.base + "/config");
296 passthru = {
297 __unstable__.self = allConfig;
298 inherit (self)
299 staticFilesImpure processedStatic
300 __iosWithConfig __androidWithConfig
301 ;
302 inherit (self.userSettings)
303 android ios overrides packages shellToolOverrides staticFiles tools withHoogle
304 __closureCompilerOptimizationLevel
305 ;
306 };
307 };
308 });
309 in allConfig;
310 in (mkProject (projectDefinition args)).projectConfig);
311 mainProjectOut = projectOut { inherit system; };
312 serverOn = projectInst: version: serverExe
313 projectInst.ghc.backend
314 mainProjectOut.ghcjs.frontend
315 projectInst.passthru.staticFiles
316 projectInst.passthru.__closureCompilerOptimizationLevel
317 version;
318 linuxExe = serverOn (projectOut { system = "x86_64-linux"; });
319 dummyVersion = "Version number is only available for deployments";
320 in mainProjectOut // {
321 __unstable__.profiledObRun = let
322 profiled = projectOut { inherit system; enableLibraryProfiling = true; };
323 exeSource = builtins.toFile "ob-run.hs" ''
324 {-# LANGUAGE NoImplicitPrelude #-}
325 {-# LANGUAGE PackageImports #-}
326 module Main where
328 import "base" Prelude
330 import Control.Exception
331 import Reflex.Profiled
332 import System.Environment
334 import qualified Obelisk.Run
335 import qualified Frontend
336 import qualified Backend
338 main :: IO ()
339 main = do
340 args <- getArgs
341 let port = read $ args !! 0
342 assets = args !! 1
343 profileFile = (args !! 2) <> ".rprof"
344 Obelisk.Run.run port (Obelisk.Run.runServeAsset assets) Backend.backend Frontend.frontend `finally` writeProfilingData profileFile
345 '';
346 in nixpkgs.runCommand "ob-run" {
347 buildInputs = [ (profiled.ghc.ghcWithPackages (p: [p.backend p.frontend])) ];
348 } ''
349 cp ${exeSource} $PWD/ob-run.hs
350 mkdir -p $out/bin/
351 ghc -x hs -prof -fno-prof-auto -threaded ob-run.hs -o $out/bin/ob-run
352 '';
354 linuxExeConfigurable = linuxExe;
355 linuxExe = linuxExe dummyVersion;
356 exe = serverOn mainProjectOut dummyVersion;
357 server = args@{ hostName, adminEmail, routeHost, enableHttps, version, module ? serverModules.mkBaseEc2 }:
358 server (args // { exe = linuxExe version; });
359 obelisk = import (base' + "/.obelisk/impl") {};
360 };
361 haskellPackageSets = {
362 inherit (reflex-platform) ghc ghcjs;
363 };