on cg@certdir help sign in
obelisk/default.nix
387 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: _: k == "tabulation" || pkgs.lib.strings.hasPrefix "obelisk-" k) ghcObeliskEnvs; #TODO: use thunkSet https://github.com/reflex-frp/reflex-platform/pull/671
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 set -euo pipefail
78 cd '${haskellLib.justStaticExecutables frontend}'
79 shopt -s globstar
80 for f in **/all.js; do
81 dir="$out/$(basename "$(dirname "$f")")"
82 mkdir -p "$dir"
83 ln -s "$(realpath "$f")" "$dir/all.unminified.js"
84 ${if optimizationLevel == null then ''
85 ln -s "$dir/all.unminified.js" "$dir/all.js"
86 '' else ''
87 '${pkgs.closurecompiler}/bin/closure-compiler' --externs '${reflex-platform.ghcjsExternsJs}' -O '${optimizationLevel}' --jscomp_warning=checkVars --create_source_map="$dir/all.js.map" --source_map_format=V3 --js_output_file="$dir/all.js" "$dir/all.unminified.js"
88 echo '//# sourceMappingURL=all.js.map' >> "$dir/all.js"
89 ''}
90 done
91 '';
93 serverModules = {
94 mkBaseEc2 = { nixosPkgs, ... }: {...}: {
95 imports = [
96 (nixosPkgs.path + /nixos/modules/virtualisation/amazon-image.nix)
97 ];
98 ec2.hvm = true;
99 };
101 mkDefaultNetworking = { adminEmail, enableHttps, hostName, routeHost, ... }: {...}: {
102 networking = {
103 inherit hostName;
104 firewall.allowedTCPPorts = if enableHttps then [ 80 443 ] else [ 80 ];
105 };
107 # `amazon-image.nix` already sets these but if the user provides their own module then
108 # forgetting these can cause them to lose access to the server!
109 # https://github.com/NixOS/nixpkgs/blob/fab05f17d15e4e125def4fd4e708d205b41d8d74/nixos/modules/virtualisation/amazon-image.nix#L133-L136
110 services.openssh.enable = true;
111 services.openssh.permitRootLogin = "prohibit-password";
113 security.acme.certs = if enableHttps then {
114 "${routeHost}".email = adminEmail;
115 } else {};
117 security.acme.${if enableHttps then "acceptTerms" else null} = true;
118 };
120 mkObeliskApp =
121 { exe
122 , routeHost
123 , enableHttps
124 , name ? "backend"
125 , user ? name
126 , group ? user
127 , baseUrl ? "/"
128 , internalPort ? 8000
129 , backendArgs ? "--port=${toString internalPort}"
130 , ...
131 }: {...}: {
132 services.nginx = {
133 enable = true;
134 recommendedProxySettings = true;
135 virtualHosts."${routeHost}" = {
136 enableACME = enableHttps;
137 forceSSL = enableHttps;
138 locations.${baseUrl} = {
139 proxyPass = "http://127.0.0.1:" + toString internalPort;
140 proxyWebsockets = true;
141 extraConfig = ''
142 access_log off;
143 '';
144 };
145 };
146 };
147 systemd.services.${name} = {
148 wantedBy = [ "multi-user.target" ];
149 after = [ "network.target" ];
150 restartIfChanged = true;
151 path = [ pkgs.gnutar ];
152 script = ''
153 ln -sft . '${exe}'/*
154 mkdir -p log
155 exec ./backend ${backendArgs} </dev/null
156 '';
157 serviceConfig = {
158 User = user;
159 KillMode = "process";
160 WorkingDirectory = "~";
161 Restart = "always";
162 RestartSec = 5;
163 };
164 };
165 users = {
166 users.${user} = {
167 description = "${user} service";
168 home = "/var/lib/${user}";
169 createHome = true;
170 isSystemUser = true;
171 group = group;
172 };
173 groups.${group} = {};
174 };
175 };
176 };
178 inherit mkAssets;
180 serverExe = backend: frontend: assets: optimizationLevel: version:
181 pkgs.runCommand "serverExe" {} ''
182 mkdir $out
183 set -eux
184 ln -s '${if profiling then backend else haskellLib.justStaticExecutables backend}'/bin/* $out/
185 ln -s '${mkAssets assets}' $out/static.assets
186 ln -s '${mkAssets (compressedJs frontend optimizationLevel)}'/* $out
187 echo ${version} > $out/version
188 '';
190 server = { exe, hostName, adminEmail, routeHost, enableHttps, version, module ? serverModules.mkBaseEc2 }@args:
191 let
192 nixos = import (pkgs.path + /nixos);
193 in nixos {
194 system = "x86_64-linux";
195 configuration = {
196 imports = [
197 (module { inherit exe hostName adminEmail routeHost enableHttps version; nixosPkgs = pkgs; })
198 (serverModules.mkDefaultNetworking args)
199 (serverModules.mkObeliskApp args)
200 ./acme.nix # Backport of ACME upgrades from 20.03
201 ];
203 # Backport of ACME upgrades from 20.03
204 disabledModules = [
205 (pkgs.path + /nixos/modules/security/acme.nix)
206 ];
207 nixpkgs.overlays = [
208 (self: super: {
209 lego = (import (builtins.fetchTarball {
210 url = https://github.com/NixOS/nixpkgs-channels/archive/70717a337f7ae4e486ba71a500367cad697e5f09.tar.gz;
211 sha256 = "1sbmqn7yc5iilqnvy9nvhsa9bx6spfq1kndvvis9031723iyymd1";
212 }) {}).lego;
213 })
214 ];
215 };
216 };
218 # An Obelisk project is a reflex-platform project with a predefined layout and role for each component
219 project = base': projectDefinition:
220 let
221 projectOut = { system, enableLibraryProfiling ? profiling }: let reflexPlatformProject = (getReflexPlatform { inherit system enableLibraryProfiling; }).project; in reflexPlatformProject (args@{ nixpkgs, ... }:
222 let
223 inherit (lib.strings) hasPrefix;
224 mkProject =
225 { android ? null #TODO: Better error when missing
226 , ios ? null #TODO: Better error when missing
227 , packages ? {}
228 , overrides ? _: _: {}
229 , staticFiles ? null
230 , tools ? _: []
231 , shellToolOverrides ? _: _: {}
232 , withHoogle ? false # Setting this to `true` makes shell reloading far slower
233 , __closureCompilerOptimizationLevel ? "ADVANCED" # Set this to `null` to skip the closure-compiler step
234 , __withGhcide ? false
235 }:
236 let
237 allConfig = nixpkgs.lib.makeExtensible (self: {
238 base = base';
239 inherit args;
240 userSettings = {
241 inherit android ios packages overrides tools shellToolOverrides withHoogle __closureCompilerOptimizationLevel __withGhcide;
242 staticFiles = if staticFiles == null then self.base + /static else staticFiles;
243 };
244 frontendName = "frontend";
245 backendName = "backend";
246 commonName = "common";
247 staticName = "obelisk-generated-static";
248 staticFilesImpure = let fs = self.userSettings.staticFiles; in if lib.isDerivation fs then fs else toString fs;
249 processedStatic = processAssets { src = self.userSettings.staticFiles; };
250 # The packages whose names and roles are defined by this package
251 predefinedPackages = lib.filterAttrs (_: x: x != null) {
252 ${self.frontendName} = nullIfAbsent (self.base + "/frontend");
253 ${self.commonName} = nullIfAbsent (self.base + "/common");
254 ${self.backendName} = nullIfAbsent (self.base + "/backend");
255 };
256 shellPackages = {};
257 combinedPackages = self.predefinedPackages // self.userSettings.packages // self.shellPackages;
258 projectOverrides = self': super': {
259 ${self.staticName} = haskellLib.dontHaddock (self'.callCabal2nix self.staticName self.processedStatic.haskellManifest {});
260 ${self.backendName} = haskellLib.addBuildDepend super'.${self.backendName} self'.obelisk-run;
261 };
262 totalOverrides = lib.composeExtensions self.projectOverrides self.userSettings.overrides;
263 privateConfigDirs = ["config/backend"];
264 injectableConfig = builtins.filterSource (path: _:
265 !(lib.lists.any (x: hasPrefix (toString self.base + "/" + toString x) (toString path)) self.privateConfigDirs)
266 );
267 __androidWithConfig = configPath: {
268 ${if self.userSettings.android == null then null else self.frontendName} = {
269 executableName = "frontend";
270 ${if builtins.pathExists self.userSettings.staticFiles then "assets" else null} =
271 nixpkgs.obeliskExecutableConfig.platforms.android.inject
272 (self.injectableConfig configPath)
273 self.processedStatic.symlinked;
274 } // self.userSettings.android;
275 };
276 __iosWithConfig = configPath: {
277 ${if self.userSettings.ios == null then null else self.frontendName} = {
278 executableName = "frontend";
279 ${if builtins.pathExists self.userSettings.staticFiles then "staticSrc" else null} =
280 nixpkgs.obeliskExecutableConfig.platforms.ios.inject
281 (self.injectableConfig configPath)
282 self.processedStatic.symlinked;
283 } // self.userSettings.ios;
284 };
286 shells-ghc = builtins.attrNames (self.predefinedPackages // self.shellPackages);
288 shells-ghcjs = [
289 self.frontendName
290 self.commonName
291 ];
293 shells-ghcSavedSplices = [
294 self.commonName
295 self.frontendName
296 ];
298 shellToolOverrides = lib.composeExtensions
299 self.userSettings.shellToolOverrides
300 (if self.userSettings.__withGhcide
301 then (import ./haskell-overlays/ghcide.nix)
302 else (_: _: {})
303 );
305 project = reflexPlatformProject ({...}: self.projectConfig);
306 projectConfig = {
307 inherit (self) shellToolOverrides;
308 inherit (self.userSettings) tools withHoogle;
309 overrides = self.totalOverrides;
310 packages = self.combinedPackages;
311 shells = {
312 ${if self.userSettings.android == null && self.userSettings.ios == null then null else "ghcSavedSplices"} =
313 lib.filter (x: lib.hasAttr x self.combinedPackages) self.shells-ghcSavedSplices;
314 ghc = lib.filter (x: lib.hasAttr x self.combinedPackages) self.shells-ghc;
315 ghcjs = lib.filter (x: lib.hasAttr x self.combinedPackages) self.shells-ghcjs;
316 };
317 android = self.__androidWithConfig (self.base + "/config");
318 ios = self.__iosWithConfig (self.base + "/config");
320 passthru = {
321 __unstable__.self = allConfig;
322 inherit (self)
323 staticFilesImpure processedStatic
324 __iosWithConfig __androidWithConfig
325 ;
326 inherit (self.userSettings)
327 android ios overrides packages shellToolOverrides staticFiles tools withHoogle
328 __closureCompilerOptimizationLevel
329 ;
330 };
331 };
332 });
333 in allConfig;
334 in (mkProject (projectDefinition args)).projectConfig);
335 mainProjectOut = projectOut { inherit system; };
336 serverOn = projectInst: version: serverExe
337 projectInst.ghc.backend
338 mainProjectOut.ghcjs.frontend
339 projectInst.passthru.staticFiles
340 projectInst.passthru.__closureCompilerOptimizationLevel
341 version;
342 linuxExe = serverOn (projectOut { system = "x86_64-linux"; });
343 dummyVersion = "Version number is only available for deployments";
344 in mainProjectOut // {
345 __unstable__.profiledObRun = let
346 profiled = projectOut { inherit system; enableLibraryProfiling = true; };
347 exeSource = builtins.toFile "ob-run.hs" ''
348 {-# LANGUAGE NoImplicitPrelude #-}
349 {-# LANGUAGE PackageImports #-}
350 module Main where
352 -- Explicitly import Prelude from base lest there be multiple modules called Prelude
353 import "base" Prelude (IO, (++), read)
355 import "base" Control.Exception (finally)
356 import "reflex" Reflex.Profiled (writeProfilingData)
357 import "base" System.Environment (getArgs)
359 import qualified "obelisk-run" Obelisk.Run
360 import qualified Frontend
361 import qualified Backend
363 main :: IO ()
364 main = do
365 [portStr, assets, profFileName] <- getArgs
366 Obelisk.Run.run (read portStr) (Obelisk.Run.runServeAsset assets) Backend.backend Frontend.frontend
367 `finally` writeProfilingData (profFileName ++ ".rprof")
368 '';
369 in nixpkgs.runCommand "ob-run" {
370 buildInputs = [ (profiled.ghc.ghcWithPackages (p: [p.backend p.frontend])) ];
371 } ''
372 cp ${exeSource} ob-run.hs
373 mkdir -p $out/bin
374 ghc -x hs -prof -fno-prof-auto -threaded ob-run.hs -o $out/bin/ob-run
375 '';
377 linuxExeConfigurable = linuxExe;
378 linuxExe = linuxExe dummyVersion;
379 exe = serverOn mainProjectOut dummyVersion;
380 server = args@{ hostName, adminEmail, routeHost, enableHttps, version, module ? serverModules.mkBaseEc2 }:
381 server (args // { exe = linuxExe version; });
382 obelisk = import (base' + "/.obelisk/impl") {};
383 };
384 haskellPackageSets = {
385 inherit (reflex-platform) ghc ghcjs;
386 };