on eac@ob-thunk-clone help sign in
obelisk/default.nix
436 lines 19 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 cleanSource = src:
15 # WARNING: The order of application here seems to matter a great deal to
16 # how quickly `ghcid` is able to reload changes. As a rule of thumb,
17 # always apply `gitignoreSource` first.
18 # See https://github.com/obsidiansystems/obelisk/pull/666 and related.
19 pkgs.lib.cleanSource (gitignoreSource src);
21 commandRuntimeDeps = pkgs: with pkgs; [
22 coreutils
23 git
24 nix-prefetch-git
25 openssh
26 ];
28 getReflexPlatform = { system, enableLibraryProfiling ? profiling }: reflex-platform-func {
29 inherit iosSdkVersion config system enableLibraryProfiling;
31 nixpkgsOverlays = [
32 (self: super: {
33 obeliskExecutableConfig = import ./lib/executable-config { nixpkgs = pkgs; filterGitSource = cleanSource; };
34 })
35 ];
37 haskellOverlays = [
39 # Fix misc upstream packages
40 (self: super: let
41 pkgs = self.callPackage ({ pkgs }: pkgs) {};
42 haskellLib = pkgs.haskell.lib;
43 in {
44 hnix = haskellLib.dontCheck (haskellLib.doJailbreak (self.callCabal2nix "hnix" (hackGet dep/hnix) {}));
45 hnix-store-core = self.callHackage "hnix-store-core" "0.1.0.0" {};
47 ghcid = self.callCabal2nix "ghcid" (hackGet ./dep/ghcid) {};
48 # Exports more internals
49 snap-core = haskellLib.dontCheck (self.callCabal2nix "snap-core" (hackGet ./dep/snap-core) {});
50 })
52 pkgs.obeliskExecutableConfig.haskellOverlay
54 # Add obelisk packages
55 (self: super: let
56 pkgs = self.callPackage ({ pkgs }: pkgs) {};
57 onLinux = pkg: f: if pkgs.stdenv.isLinux then f pkg else pkg;
58 in {
59 shelly = self.callHackage "shelly" "1.9.0" {};
61 obelisk-executable-config-inject = pkgs.obeliskExecutableConfig.platforms.web.inject self;
63 obelisk-asset-manifest = self.callCabal2nix "obelisk-asset-manifest" (cleanSource ./lib/asset/manifest) {};
64 obelisk-asset-serve-snap = self.callCabal2nix "obelisk-asset-serve-snap" (cleanSource ./lib/asset/serve-snap) {};
65 obelisk-backend = self.callCabal2nix "obelisk-backend" (cleanSource ./lib/backend) {};
66 obelisk-cliapp = self.callCabal2nix "obelisk-cliapp" (cleanSource ./lib/cliapp) {};
67 obelisk-command = haskellLib.overrideCabal (self.callCabal2nix "obelisk-command" (cleanSource ./lib/command) {}) {
68 librarySystemDepends = [
69 pkgs.jre
70 pkgs.nix
71 (haskellLib.justStaticExecutables self.ghcid)
72 ];
73 };
74 obelisk-frontend = self.callCabal2nix "obelisk-frontend" (cleanSource ./lib/frontend) {};
75 obelisk-run = onLinux (self.callCabal2nix "obelisk-run" (cleanSource ./lib/run) {}) (pkg:
76 haskellLib.overrideCabal pkg (drv: { librarySystemDepends = [ pkgs.iproute ]; })
77 );
78 obelisk-route = self.callCabal2nix "obelisk-route" (cleanSource ./lib/route) {};
79 obelisk-selftest = haskellLib.overrideCabal (self.callCabal2nix "obelisk-selftest" (cleanSource ./lib/selftest) {}) {
80 librarySystemDepends = [
81 pkgs.cabal-install
82 pkgs.coreutils
83 pkgs.git
84 pkgs.nix
85 pkgs.rsync
86 ];
87 };
88 obelisk-snap-extras = self.callCabal2nix "obelisk-snap-extras" (cleanSource ./lib/snap-extras) {};
89 tabulation = self.callCabal2nix "tabulation" (cleanSource ./lib/tabulation) {};
90 })
92 (self: super: let
93 pkgs = self.callPackage ({ pkgs }: pkgs) {};
94 haskellLib = pkgs.haskell.lib;
95 in {
96 # Dynamic linking with split objects dramatically increases startup time (about
97 # 0.5 seconds on a decent machine with SSD), so we do `justStaticExecutables`.
98 obelisk-command = haskellLib.overrideCabal
99 (haskellLib.generateOptparseApplicativeCompletion "ob"
100 (haskellLib.justStaticExecutables super.obelisk-command))
101 (drv: {
102 buildTools = (drv.buildTools or []) ++ [ pkgs.buildPackages.makeWrapper ];
103 postFixup = ''
104 ${drv.postFixup or ""}
105 # Make `ob` reference its runtime dependencies.
106 wrapProgram "$out"/bin/ob --prefix PATH : ${pkgs.lib.makeBinPath (commandRuntimeDeps pkgs)}
107 '';
108 });
109 obelisk-selftest = haskellLib.justStaticExecutables super.obelisk-selftest;
110 })
112 ];
113 };
115 # The haskell environment used to build Obelisk itself, e.g. the 'ob' command
116 ghcObelisk = reflex-platform.ghc;
118 # Development environments for obelisk packages.
119 ghcObeliskEnvs = pkgs.lib.mapAttrs (n: v: reflex-platform.workOn ghcObelisk v) ghcObelisk;
121 inherit (import ./lib/asset/assets.nix { inherit nixpkgs; }) mkAssets;
123 haskellLib = pkgs.haskell.lib;
125in rec {
126 inherit reflex-platform;
127 inherit (reflex-platform) nixpkgs pinBuildInputs;
128 inherit (nixpkgs) lib;
129 pathGit = ./.; # Used in CI by the migration graph hash algorithm to correctly ignore files.
130 path = reflex-platform.filterGit ./.;
131 obelisk = ghcObelisk;
132 obeliskEnvs = pkgs.lib.filterAttrs (k: _: pkgs.lib.strings.hasPrefix "obelisk-" k) ghcObeliskEnvs;
133 command = ghcObelisk.obelisk-command;
134 shell = pinBuildInputs "obelisk-shell" ([command] ++ commandRuntimeDeps pkgs);
136 selftest = pkgs.writeScript "selftest" ''
137 #!${pkgs.runtimeShell}
138 set -euo pipefail
140 PATH="${command}/bin:$PATH"
141 cd ${./.}
142 "${ghcObelisk.obelisk-selftest}/bin/obelisk-selftest" +RTS -N -RTS "$@"
143 '';
144 skeleton = pkgs.runCommand "skeleton" {
145 dir = builtins.filterSource (path: type: builtins.trace path (baseNameOf path != ".obelisk")) ./skeleton;
146 } ''
147 ln -s "$dir" "$out"
148 '';
149 nullIfAbsent = p: if lib.pathExists p then p else null;
150 #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.
151 processAssets = { src, packageName ? "obelisk-generated-static", moduleName ? "Obelisk.Generated.Static" }: pkgs.runCommand "asset-manifest" {
152 inherit src;
153 outputs = [ "out" "haskellManifest" "symlinked" ];
154 nativeBuildInputs = [ ghcObelisk.obelisk-asset-manifest ];
155 } ''
156 set -euo pipefail
157 touch "$out"
158 mkdir -p "$symlinked"
159 obelisk-asset-manifest-generate "$src" "$haskellManifest" ${packageName} ${moduleName} "$symlinked"
160 '';
162 compressedJs = frontend: optimizationLevel: pkgs.runCommand "compressedJs" {} ''
163 mkdir $out
164 cd $out
165 # TODO profiling + static shouldn't break and need an ad-hoc workaround like that
166 ln -s "${haskellLib.justStaticExecutables frontend}/bin/frontend.jsexe/all.js" all.unminified.js
167 ${if optimizationLevel == null then ''
168 ln -s all.unminified.js all.js
169 '' else ''
170 ${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
171 echo "//# sourceMappingURL=all.js.map" >> all.js
172 ''}
173 '';
175 serverModules = {
176 mkBaseEc2 = { nixosPkgs, ... }: {...}: {
177 imports = [
178 (nixosPkgs.path + /nixos/modules/virtualisation/amazon-image.nix)
179 ];
180 ec2.hvm = true;
181 };
183 mkDefaultNetworking = { adminEmail, enableHttps, hostName, routeHost, ... }: {...}: {
184 networking = {
185 inherit hostName;
186 firewall.allowedTCPPorts = if enableHttps then [ 80 443 ] else [ 80 ];
187 };
189 # `amazon-image.nix` already sets these but if the user provides their own module then
190 # forgetting these can cause them to lose access to the server!
191 # https://github.com/NixOS/nixpkgs/blob/fab05f17d15e4e125def4fd4e708d205b41d8d74/nixos/modules/virtualisation/amazon-image.nix#L133-L136
192 services.openssh.enable = true;
193 services.openssh.permitRootLogin = "prohibit-password";
195 security.acme.certs = if enableHttps then {
196 "${routeHost}".email = adminEmail;
197 } else {};
198 };
200 mkObeliskApp =
201 { exe
202 , routeHost
203 , enableHttps
204 , name ? "backend"
205 , user ? name
206 , group ? user
207 , baseUrl ? "/"
208 , internalPort ? 8000
209 , backendArgs ? "--port=${toString internalPort}"
210 , ...
211 }: {...}: {
212 services.nginx = {
213 enable = true;
214 virtualHosts."${routeHost}" = {
215 enableACME = enableHttps;
216 forceSSL = enableHttps;
217 locations.${baseUrl} = {
218 proxyPass = "http://127.0.0.1:" + toString internalPort;
219 proxyWebsockets = true;
220 };
221 };
222 };
223 systemd.services.${name} = {
224 wantedBy = [ "multi-user.target" ];
225 after = [ "network.target" ];
226 restartIfChanged = true;
227 path = [ pkgs.gnutar ];
228 script = ''
229 ln -sft . '${exe}'/*
230 mkdir -p log
231 exec ./backend ${backendArgs} </dev/null
232 '';
233 serviceConfig = {
234 User = user;
235 KillMode = "process";
236 WorkingDirectory = "~";
237 Restart = "always";
238 RestartSec = 5;
239 };
240 };
241 users = {
242 users.${user} = {
243 description = "${user} service";
244 home = "/var/lib/${user}";
245 createHome = true;
246 isSystemUser = true;
247 group = group;
248 };
249 groups.${group} = {};
250 };
251 };
252 };
254 inherit mkAssets;
256 serverExe = backend: frontend: assets: optimizationLevel: version:
257 pkgs.runCommand "serverExe" {} ''
258 mkdir $out
259 set -eux
260 ln -s "${if profiling then backend else haskellLib.justStaticExecutables backend}"/bin/* $out/
261 ln -s "${mkAssets assets}" $out/static.assets
262 ln -s ${mkAssets (compressedJs frontend optimizationLevel)} $out/frontend.jsexe.assets
263 echo ${version} > $out/version
264 '';
266 server = { exe, hostName, adminEmail, routeHost, enableHttps, version, module ? serverModules.mkBaseEc2 }@args:
267 let
268 nixos = import (pkgs.path + /nixos);
269 in nixos {
270 system = "x86_64-linux";
271 configuration = {
272 imports = [
273 (module { inherit exe hostName adminEmail routeHost enableHttps version; nixosPkgs = pkgs; })
274 (serverModules.mkDefaultNetworking args)
275 (serverModules.mkObeliskApp args)
276 ];
277 };
278 };
280 # An Obelisk project is a reflex-platform project with a predefined layout and role for each component
281 project = base': projectDefinition:
282 let
283 projectOut = { system, enableLibraryProfiling ? profiling }: let reflexPlatformProject = (getReflexPlatform { inherit system enableLibraryProfiling; }).project; in reflexPlatformProject (args@{ nixpkgs, ... }:
284 let
285 inherit (lib.strings) hasPrefix;
286 mkProject =
287 { android ? null #TODO: Better error when missing
288 , ios ? null #TODO: Better error when missing
289 , packages ? {}
290 , overrides ? _: _: {}
291 , staticFiles ? null
292 , tools ? _: []
293 , shellToolOverrides ? _: _: {}
294 , withHoogle ? false # Setting this to `true` makes shell reloading far slower
295 , __closureCompilerOptimizationLevel ? "ADVANCED" # Set this to `null` to skip the closure-compiler step
296 }:
297 let
298 allConfig = nixpkgs.lib.makeExtensible (self: {
299 base = base';
300 inherit args;
301 userSettings = {
302 inherit android ios packages overrides tools shellToolOverrides withHoogle __closureCompilerOptimizationLevel;
303 staticFiles = if staticFiles == null then self.base + /static else staticFiles;
304 };
305 frontendName = "frontend";
306 backendName = "backend";
307 commonName = "common";
308 staticName = "obelisk-generated-static";
309 staticFilesImpure = let fs = self.userSettings.staticFiles; in if lib.isDerivation fs then fs else toString fs;
310 processedStatic = processAssets { src = self.userSettings.staticFiles; };
311 # The packages whose names and roles are defined by this package
312 predefinedPackages = lib.filterAttrs (_: x: x != null) {
313 ${self.frontendName} = nullIfAbsent (self.base + "/frontend");
314 ${self.commonName} = nullIfAbsent (self.base + "/common");
315 ${self.backendName} = nullIfAbsent (self.base + "/backend");
316 };
317 shellPackages = {};
318 combinedPackages = self.predefinedPackages // self.userSettings.packages // self.shellPackages;
319 projectOverrides = self': super': {
320 ${self.staticName} = haskellLib.dontHaddock (self'.callCabal2nix self.staticName self.processedStatic.haskellManifest {});
321 ${self.backendName} = haskellLib.addBuildDepend super'.${self.backendName} self'.obelisk-run;
322 };
323 totalOverrides = lib.composeExtensions self.projectOverrides self.userSettings.overrides;
324 privateConfigDirs = ["config/backend"];
325 injectableConfig = builtins.filterSource (path: _:
326 !(lib.lists.any (x: hasPrefix (toString self.base + "/" + toString x) (toString path)) self.privateConfigDirs)
327 );
328 __androidWithConfig = configPath: {
329 ${if self.userSettings.android == null then null else self.frontendName} = {
330 executableName = "frontend";
331 ${if builtins.pathExists self.userSettings.staticFiles then "assets" else null} =
332 nixpkgs.obeliskExecutableConfig.platforms.android.inject
333 (self.injectableConfig configPath)
334 self.processedStatic.symlinked;
335 } // self.userSettings.android;
336 };
337 __iosWithConfig = configPath: {
338 ${if self.userSettings.ios == null then null else self.frontendName} = {
339 executableName = "frontend";
340 ${if builtins.pathExists self.userSettings.staticFiles then "staticSrc" else null} =
341 nixpkgs.obeliskExecutableConfig.platforms.ios.inject
342 (self.injectableConfig configPath)
343 self.processedStatic.symlinked;
344 } // self.userSettings.ios;
345 };
347 shells-ghc = builtins.attrNames (self.predefinedPackages // self.shellPackages);
349 shells-ghcjs = [
350 self.frontendName
351 self.commonName
352 ];
354 shells-ghcSavedSplices = [
355 self.commonName
356 self.frontendName
357 ];
359 project = reflexPlatformProject ({...}: self.projectConfig);
360 projectConfig = {
361 inherit (self.userSettings) shellToolOverrides tools withHoogle;
362 overrides = self.totalOverrides;
363 packages = self.combinedPackages;
364 shells = {
365 ${if self.userSettings.android == null && self.userSettings.ios == null then null else "ghcSavedSplices"} =
366 lib.filter (x: lib.hasAttr x self.combinedPackages) self.shells-ghcSavedSplices;
367 ghc = lib.filter (x: lib.hasAttr x self.combinedPackages) self.shells-ghc;
368 ghcjs = lib.filter (x: lib.hasAttr x self.combinedPackages) self.shells-ghcjs;
369 };
370 android = self.__androidWithConfig (self.base + "/config");
371 ios = self.__iosWithConfig (self.base + "/config");
373 passthru = {
374 __unstable__.self = allConfig;
375 inherit (self)
376 staticFilesImpure processedStatic
377 __iosWithConfig __androidWithConfig
378 ;
379 inherit (self.userSettings)
380 android ios overrides packages shellToolOverrides staticFiles tools withHoogle
381 __closureCompilerOptimizationLevel
382 ;
383 };
384 };
385 });
386 in allConfig;
387 in (mkProject (projectDefinition args)).projectConfig);
388 mainProjectOut = projectOut { inherit system; };
389 serverOn = projectInst: version: serverExe
390 projectInst.ghc.backend
391 mainProjectOut.ghcjs.frontend
392 projectInst.passthru.staticFiles
393 projectInst.passthru.__closureCompilerOptimizationLevel
394 version;
395 linuxExe = serverOn (projectOut { system = "x86_64-linux"; });
396 dummyVersion = "Version number is only available for deployments";
397 in mainProjectOut // {
398 __unstable__.profiledObRun = let
399 profiled = projectOut { inherit system; enableLibraryProfiling = true; };
400 exeSource = builtins.toFile "ob-run.hs" ''
401 module Main where
403 import Control.Exception
404 import Reflex.Profiled
405 import System.Environment
407 import qualified Obelisk.Run
408 import qualified Frontend
409 import qualified Backend
411 main :: IO ()
412 main = do
413 args <- getArgs
414 let port = read $ args !! 0
415 assets = args !! 1
416 profileFile = (args !! 2) <> ".rprof"
417 Obelisk.Run.run port (Obelisk.Run.runServeAsset assets) Backend.backend Frontend.frontend `finally` writeProfilingData profileFile
418 '';
419 in nixpkgs.runCommand "ob-run" {
420 buildInputs = [ (profiled.ghc.ghcWithPackages (p: [ p.backend p.frontend])) ];
421 } ''
422 mkdir -p $out/bin/
423 ghc -x hs -prof -fno-prof-auto -threaded ${exeSource} -o $out/bin/ob-run
424 '';
426 linuxExeConfigurable = linuxExe;
427 linuxExe = linuxExe dummyVersion;
428 exe = serverOn mainProjectOut dummyVersion;
429 server = args@{ hostName, adminEmail, routeHost, enableHttps, version, module ? serverModules.mkBaseEc2 }:
430 server (args // { exe = linuxExe version; });
431 obelisk = import (base' + "/.obelisk/impl") {};
432 };
433 haskellPackageSets = {
434 inherit (reflex-platform) ghc ghcjs;
435 };