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