on eac@proper-shell help sign in
obelisk/default.nix
367 lines 16 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 getLocalSourceSet = pkg: lib.filter
195 (p: (builtins.isPath or (x: !(builtins.isString x))) (p.src or null))
196 (lib.closePropagation (
197 pkg.getBuildInputs.haskellBuildInputs or
198 (pkg.buildInputs ++ pkg.propagatedBuildInputs ++ pkg.nativeBuildInputs)
199 ));
201 # An Obelisk project is a reflex-platform project with a predefined layout and role for each component
202 project = base': projectDefinition:
203 let
204 projectOut = { system, enableLibraryProfiling ? profiling }: let reflexPlatformProject = (getReflexPlatform { inherit system enableLibraryProfiling; }).project; in reflexPlatformProject (args@{ nixpkgs, ... }:
205 let
206 inherit (lib.strings) hasPrefix;
207 mkProject =
208 { android ? null #TODO: Better error when missing
209 , ios ? null #TODO: Better error when missing
210 , packages ? {}
211 , overrides ? _: _: {}
212 , staticFiles ? null
213 , tools ? _: []
214 , shellToolOverrides ? _: _: {}
215 , withHoogle ? false # Setting this to `true` makes shell reloading far slower
216 , __closureCompilerOptimizationLevel ? "ADVANCED" # Set this to `null` to skip the closure-compiler step
217 , __withGhcide ? false
218 }:
219 let
220 allConfig = nixpkgs.lib.makeExtensible (self: {
221 base = base';
222 inherit args;
223 userSettings = {
224 inherit android ios packages overrides tools shellToolOverrides withHoogle __closureCompilerOptimizationLevel __withGhcide;
225 staticFiles = if staticFiles == null then self.base + /static else staticFiles;
226 };
227 frontendName = "frontend";
228 backendName = "backend";
229 commonName = "common";
230 staticName = "obelisk-generated-static";
231 staticFilesImpure = let fs = self.userSettings.staticFiles; in if lib.isDerivation fs then fs else toString fs;
232 processedStatic = processAssets { src = self.userSettings.staticFiles; };
233 # The packages whose names and roles are defined by this package
234 predefinedPackages = lib.filterAttrs (_: x: x != null) {
235 ${self.frontendName} = nullIfAbsent (self.base + "/frontend");
236 ${self.commonName} = nullIfAbsent (self.base + "/common");
237 ${self.backendName} = nullIfAbsent (self.base + "/backend");
238 };
239 shellPackages = {};
240 combinedPackages = self.predefinedPackages // self.userSettings.packages // self.shellPackages;
241 projectOverrides = self': super': {
242 ${self.staticName} = haskellLib.dontHaddock (self'.callCabal2nix self.staticName self.processedStatic.haskellManifest {});
243 ${self.backendName} = haskellLib.addBuildDepend super'.${self.backendName} self'.obelisk-run;
244 };
245 totalOverrides = lib.composeExtensions self.projectOverrides self.userSettings.overrides;
246 privateConfigDirs = ["config/backend"];
247 injectableConfig = builtins.filterSource (path: _:
248 !(lib.lists.any (x: hasPrefix (toString self.base + "/" + toString x) (toString path)) self.privateConfigDirs)
249 );
250 __androidWithConfig = configPath: {
251 ${if self.userSettings.android == null then null else self.frontendName} = {
252 executableName = "frontend";
253 ${if builtins.pathExists self.userSettings.staticFiles then "assets" else null} =
254 nixpkgs.obeliskExecutableConfig.platforms.android.inject
255 (self.injectableConfig configPath)
256 self.processedStatic.symlinked;
257 } // self.userSettings.android;
258 };
259 __iosWithConfig = configPath: {
260 ${if self.userSettings.ios == null then null else self.frontendName} = {
261 executableName = "frontend";
262 ${if builtins.pathExists self.userSettings.staticFiles then "staticSrc" else null} =
263 nixpkgs.obeliskExecutableConfig.platforms.ios.inject
264 (self.injectableConfig configPath)
265 self.processedStatic.symlinked;
266 } // self.userSettings.ios;
267 };
269 shells-ghc = builtins.attrNames (self.predefinedPackages // self.shellPackages);
271 shells-ghcjs = [
272 self.frontendName
273 self.commonName
274 ];
276 shells-ghcSavedSplices = [
277 self.commonName
278 self.frontendName
279 ];
281 shellToolOverrides = lib.composeExtensions
282 self.userSettings.shellToolOverrides
283 (if self.userSettings.__withGhcide
284 then (import ./haskell-overlays/ghcide.nix)
285 else (_: _: {})
286 );
288 project = reflexPlatformProject ({...}: self.projectConfig);
289 projectConfig = {
290 inherit (self) shellToolOverrides;
291 inherit (self.userSettings) tools withHoogle;
292 overrides = self.totalOverrides;
293 packages = self.combinedPackages;
294 shells = {
295 ${if self.userSettings.android == null && self.userSettings.ios == null then null else "ghcSavedSplices"} =
296 lib.filter (x: lib.hasAttr x self.combinedPackages) self.shells-ghcSavedSplices;
297 ghc = lib.filter (x: lib.hasAttr x self.combinedPackages) self.shells-ghc;
298 ghcjs = lib.filter (x: lib.hasAttr x self.combinedPackages) self.shells-ghcjs;
299 };
300 android = self.__androidWithConfig (self.base + "/config");
301 ios = self.__iosWithConfig (self.base + "/config");
303 passthru = {
304 __unstable__.self = allConfig;
305 inherit (self)
306 staticFilesImpure processedStatic
307 __iosWithConfig __androidWithConfig
308 ;
309 inherit (self.userSettings)
310 android ios overrides packages shellToolOverrides staticFiles tools withHoogle
311 __closureCompilerOptimizationLevel
312 ;
313 };
314 };
315 });
316 in allConfig;
317 in (mkProject (projectDefinition args)).projectConfig);
318 mainProjectOut = projectOut { inherit system; };
319 serverOn = projectInst: version: serverExe
320 projectInst.ghc.backend
321 mainProjectOut.ghcjs.frontend
322 projectInst.passthru.staticFiles
323 projectInst.passthru.__closureCompilerOptimizationLevel
324 version;
325 linuxExe = serverOn (projectOut { system = "x86_64-linux"; });
326 dummyVersion = "Version number is only available for deployments";
327 in mainProjectOut // {
328 __unstable__.profiledObRun = let
329 profiled = projectOut { inherit system; enableLibraryProfiling = true; };
330 exeSource = builtins.toFile "ob-run.hs" ''
331 module Main where
333 import Control.Exception
334 import Reflex.Profiled
335 import System.Environment
337 import qualified Obelisk.Run
338 import qualified Frontend
339 import qualified Backend
341 main :: IO ()
342 main = do
343 args <- getArgs
344 let port = read $ args !! 0
345 assets = args !! 1
346 profileFile = (args !! 2) <> ".rprof"
347 Obelisk.Run.run port (Obelisk.Run.runServeAsset assets) Backend.backend Frontend.frontend `finally` writeProfilingData profileFile
348 '';
349 in nixpkgs.runCommand "ob-run" {
350 buildInputs = [ (profiled.ghc.ghcWithPackages (p: [ p.backend p.frontend])) ];
351 } ''
352 cp ${exeSource} $PWD/ob-run.hs
353 mkdir -p $out/bin/
354 ghc -x hs -prof -fno-prof-auto -threaded ob-run.hs -o $out/bin/ob-run
355 '';
357 linuxExeConfigurable = linuxExe;
358 linuxExe = linuxExe dummyVersion;
359 exe = serverOn mainProjectOut dummyVersion;
360 server = args@{ hostName, adminEmail, routeHost, enableHttps, version, module ? serverModules.mkBaseEc2 }:
361 server (args // { exe = linuxExe version; });
362 obelisk = import (base' + "/.obelisk/impl") {};
363 };
364 haskellPackageSets = {
365 inherit (reflex-platform) ghc ghcjs;
366 };