on v0.7.0.1 help sign in
obelisk/default.nix
435 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 ];
86 };
87 obelisk-snap-extras = self.callCabal2nix "obelisk-snap-extras" (cleanSource ./lib/snap-extras) {};
88 tabulation = self.callCabal2nix "tabulation" (cleanSource ./lib/tabulation) {};
89 })
91 (self: super: let
92 pkgs = self.callPackage ({ pkgs }: pkgs) {};
93 haskellLib = pkgs.haskell.lib;
94 in {
95 # Dynamic linking with split objects dramatically increases startup time (about
96 # 0.5 seconds on a decent machine with SSD), so we do `justStaticExecutables`.
97 obelisk-command = haskellLib.overrideCabal
98 (haskellLib.generateOptparseApplicativeCompletion "ob"
99 (haskellLib.justStaticExecutables super.obelisk-command))
100 (drv: {
101 buildTools = (drv.buildTools or []) ++ [ pkgs.buildPackages.makeWrapper ];
102 postFixup = ''
103 ${drv.postFixup or ""}
104 # Make `ob` reference its runtime dependencies.
105 wrapProgram "$out"/bin/ob --prefix PATH : ${pkgs.lib.makeBinPath (commandRuntimeDeps pkgs)}
106 '';
107 });
108 obelisk-selftest = haskellLib.justStaticExecutables super.obelisk-selftest;
109 })
111 ];
112 };
114 # The haskell environment used to build Obelisk itself, e.g. the 'ob' command
115 ghcObelisk = reflex-platform.ghc;
117 # Development environments for obelisk packages.
118 ghcObeliskEnvs = pkgs.lib.mapAttrs (n: v: reflex-platform.workOn ghcObelisk v) ghcObelisk;
120 inherit (import ./lib/asset/assets.nix { inherit nixpkgs; }) mkAssets;
122 haskellLib = pkgs.haskell.lib;
124in rec {
125 inherit reflex-platform;
126 inherit (reflex-platform) nixpkgs pinBuildInputs;
127 inherit (nixpkgs) lib;
128 pathGit = ./.; # Used in CI by the migration graph hash algorithm to correctly ignore files.
129 path = reflex-platform.filterGit ./.;
130 obelisk = ghcObelisk;
131 obeliskEnvs = pkgs.lib.filterAttrs (k: _: pkgs.lib.strings.hasPrefix "obelisk-" k) ghcObeliskEnvs;
132 command = ghcObelisk.obelisk-command;
133 shell = pinBuildInputs "obelisk-shell" ([command] ++ commandRuntimeDeps pkgs);
135 selftest = pkgs.writeScript "selftest" ''
136 #!${pkgs.runtimeShell}
137 set -euo pipefail
139 PATH="${command}/bin:$PATH"
140 cd ${./.}
141 "${ghcObelisk.obelisk-selftest}/bin/obelisk-selftest" +RTS -N -RTS "$@"
142 '';
143 skeleton = pkgs.runCommand "skeleton" {
144 dir = builtins.filterSource (path: type: builtins.trace path (baseNameOf path != ".obelisk")) ./skeleton;
145 } ''
146 ln -s "$dir" "$out"
147 '';
148 nullIfAbsent = p: if lib.pathExists p then p else null;
149 #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.
150 processAssets = { src, packageName ? "obelisk-generated-static", moduleName ? "Obelisk.Generated.Static" }: pkgs.runCommand "asset-manifest" {
151 inherit src;
152 outputs = [ "out" "haskellManifest" "symlinked" ];
153 nativeBuildInputs = [ ghcObelisk.obelisk-asset-manifest ];
154 } ''
155 set -euo pipefail
156 touch "$out"
157 mkdir -p "$symlinked"
158 obelisk-asset-manifest-generate "$src" "$haskellManifest" ${packageName} ${moduleName} "$symlinked"
159 '';
161 compressedJs = frontend: optimizationLevel: pkgs.runCommand "compressedJs" {} ''
162 mkdir $out
163 cd $out
164 # TODO profiling + static shouldn't break and need an ad-hoc workaround like that
165 ln -s "${haskellLib.justStaticExecutables frontend}/bin/frontend.jsexe/all.js" all.unminified.js
166 ${if optimizationLevel == null then ''
167 ln -s all.unminified.js all.js
168 '' else ''
169 ${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
170 echo "//# sourceMappingURL=all.js.map" >> all.js
171 ''}
172 '';
174 serverModules = {
175 mkBaseEc2 = { nixosPkgs, ... }: {...}: {
176 imports = [
177 (nixosPkgs.path + /nixos/modules/virtualisation/amazon-image.nix)
178 ];
179 ec2.hvm = true;
180 };
182 mkDefaultNetworking = { adminEmail, enableHttps, hostName, routeHost, ... }: {...}: {
183 networking = {
184 inherit hostName;
185 firewall.allowedTCPPorts = if enableHttps then [ 80 443 ] else [ 80 ];
186 };
188 # `amazon-image.nix` already sets these but if the user provides their own module then
189 # forgetting these can cause them to lose access to the server!
190 # https://github.com/NixOS/nixpkgs/blob/fab05f17d15e4e125def4fd4e708d205b41d8d74/nixos/modules/virtualisation/amazon-image.nix#L133-L136
191 services.openssh.enable = true;
192 services.openssh.permitRootLogin = "prohibit-password";
194 security.acme.certs = if enableHttps then {
195 "${routeHost}".email = adminEmail;
196 } else {};
197 };
199 mkObeliskApp =
200 { exe
201 , routeHost
202 , enableHttps
203 , name ? "backend"
204 , user ? name
205 , group ? user
206 , baseUrl ? "/"
207 , internalPort ? 8000
208 , backendArgs ? "--port=${toString internalPort}"
209 , ...
210 }: {...}: {
211 services.nginx = {
212 enable = true;
213 virtualHosts."${routeHost}" = {
214 enableACME = enableHttps;
215 forceSSL = enableHttps;
216 locations.${baseUrl} = {
217 proxyPass = "http://127.0.0.1:" + toString internalPort;
218 proxyWebsockets = true;
219 };
220 };
221 };
222 systemd.services.${name} = {
223 wantedBy = [ "multi-user.target" ];
224 after = [ "network.target" ];
225 restartIfChanged = true;
226 path = [ pkgs.gnutar ];
227 script = ''
228 ln -sft . '${exe}'/*
229 mkdir -p log
230 exec ./backend ${backendArgs} </dev/null
231 '';
232 serviceConfig = {
233 User = user;
234 KillMode = "process";
235 WorkingDirectory = "~";
236 Restart = "always";
237 RestartSec = 5;
238 };
239 };
240 users = {
241 users.${user} = {
242 description = "${user} service";
243 home = "/var/lib/${user}";
244 createHome = true;
245 isSystemUser = true;
246 group = group;
247 };
248 groups.${group} = {};
249 };
250 };
251 };
253 inherit mkAssets;
255 serverExe = backend: frontend: assets: optimizationLevel: version:
256 pkgs.runCommand "serverExe" {} ''
257 mkdir $out
258 set -eux
259 ln -s "${if profiling then backend else haskellLib.justStaticExecutables backend}"/bin/* $out/
260 ln -s "${mkAssets assets}" $out/static.assets
261 ln -s ${mkAssets (compressedJs frontend optimizationLevel)} $out/frontend.jsexe.assets
262 echo ${version} > $out/version
263 '';
265 server = { exe, hostName, adminEmail, routeHost, enableHttps, version, module ? serverModules.mkBaseEc2 }@args:
266 let
267 nixos = import (pkgs.path + /nixos);
268 in nixos {
269 system = "x86_64-linux";
270 configuration = {
271 imports = [
272 (module { inherit exe hostName adminEmail routeHost enableHttps version; nixosPkgs = pkgs; })
273 (serverModules.mkDefaultNetworking args)
274 (serverModules.mkObeliskApp args)
275 ];
276 };
277 };
279 # An Obelisk project is a reflex-platform project with a predefined layout and role for each component
280 project = base': projectDefinition:
281 let
282 projectOut = { system, enableLibraryProfiling ? profiling }: let reflexPlatformProject = (getReflexPlatform { inherit system enableLibraryProfiling; }).project; in reflexPlatformProject (args@{ nixpkgs, ... }:
283 let
284 inherit (lib.strings) hasPrefix;
285 mkProject =
286 { android ? null #TODO: Better error when missing
287 , ios ? null #TODO: Better error when missing
288 , packages ? {}
289 , overrides ? _: _: {}
290 , staticFiles ? null
291 , tools ? _: []
292 , shellToolOverrides ? _: _: {}
293 , withHoogle ? false # Setting this to `true` makes shell reloading far slower
294 , __closureCompilerOptimizationLevel ? "ADVANCED" # Set this to `null` to skip the closure-compiler step
295 }:
296 let
297 allConfig = nixpkgs.lib.makeExtensible (self: {
298 base = base';
299 inherit args;
300 userSettings = {
301 inherit android ios packages overrides tools shellToolOverrides withHoogle __closureCompilerOptimizationLevel;
302 staticFiles = if staticFiles == null then self.base + /static else staticFiles;
303 };
304 frontendName = "frontend";
305 backendName = "backend";
306 commonName = "common";
307 staticName = "obelisk-generated-static";
308 staticFilesImpure = let fs = self.userSettings.staticFiles; in if lib.isDerivation fs then fs else toString fs;
309 processedStatic = processAssets { src = self.userSettings.staticFiles; };
310 # The packages whose names and roles are defined by this package
311 predefinedPackages = lib.filterAttrs (_: x: x != null) {
312 ${self.frontendName} = nullIfAbsent (self.base + "/frontend");
313 ${self.commonName} = nullIfAbsent (self.base + "/common");
314 ${self.backendName} = nullIfAbsent (self.base + "/backend");
315 };
316 shellPackages = {};
317 combinedPackages = self.predefinedPackages // self.userSettings.packages // self.shellPackages;
318 projectOverrides = self': super': {
319 ${self.staticName} = haskellLib.dontHaddock (self'.callCabal2nix self.staticName self.processedStatic.haskellManifest {});
320 ${self.backendName} = haskellLib.addBuildDepend super'.${self.backendName} self'.obelisk-run;
321 };
322 totalOverrides = lib.composeExtensions self.projectOverrides self.userSettings.overrides;
323 privateConfigDirs = ["config/backend"];
324 injectableConfig = builtins.filterSource (path: _:
325 !(lib.lists.any (x: hasPrefix (toString self.base + "/" + toString x) (toString path)) self.privateConfigDirs)
326 );
327 __androidWithConfig = configPath: {
328 ${if self.userSettings.android == null then null else self.frontendName} = {
329 executableName = "frontend";
330 ${if builtins.pathExists self.userSettings.staticFiles then "assets" else null} =
331 nixpkgs.obeliskExecutableConfig.platforms.android.inject
332 (self.injectableConfig configPath)
333 self.processedStatic.symlinked;
334 } // self.userSettings.android;
335 };
336 __iosWithConfig = configPath: {
337 ${if self.userSettings.ios == null then null else self.frontendName} = {
338 executableName = "frontend";
339 ${if builtins.pathExists self.userSettings.staticFiles then "staticSrc" else null} =
340 nixpkgs.obeliskExecutableConfig.platforms.ios.inject
341 (self.injectableConfig configPath)
342 self.processedStatic.symlinked;
343 } // self.userSettings.ios;
344 };
346 shells-ghc = builtins.attrNames (self.predefinedPackages // self.shellPackages);
348 shells-ghcjs = [
349 self.frontendName
350 self.commonName
351 ];
353 shells-ghcSavedSplices = [
354 self.commonName
355 self.frontendName
356 ];
358 project = reflexPlatformProject ({...}: self.projectConfig);
359 projectConfig = {
360 inherit (self.userSettings) shellToolOverrides tools withHoogle;
361 overrides = self.totalOverrides;
362 packages = self.combinedPackages;
363 shells = {
364 ${if self.userSettings.android == null && self.userSettings.ios == null then null else "ghcSavedSplices"} =
365 lib.filter (x: lib.hasAttr x self.combinedPackages) self.shells-ghcSavedSplices;
366 ghc = lib.filter (x: lib.hasAttr x self.combinedPackages) self.shells-ghc;
367 ghcjs = lib.filter (x: lib.hasAttr x self.combinedPackages) self.shells-ghcjs;
368 };
369 android = self.__androidWithConfig (self.base + "/config");
370 ios = self.__iosWithConfig (self.base + "/config");
372 passthru = {
373 __unstable__.self = allConfig;
374 inherit (self)
375 staticFilesImpure processedStatic
376 __iosWithConfig __androidWithConfig
377 ;
378 inherit (self.userSettings)
379 android ios overrides packages shellToolOverrides staticFiles tools withHoogle
380 __closureCompilerOptimizationLevel
381 ;
382 };
383 };
384 });
385 in allConfig;
386 in (mkProject (projectDefinition args)).projectConfig);
387 mainProjectOut = projectOut { inherit system; };
388 serverOn = projectInst: version: serverExe
389 projectInst.ghc.backend
390 mainProjectOut.ghcjs.frontend
391 projectInst.passthru.staticFiles
392 projectInst.passthru.__closureCompilerOptimizationLevel
393 version;
394 linuxExe = serverOn (projectOut { system = "x86_64-linux"; });
395 dummyVersion = "Version number is only available for deployments";
396 in mainProjectOut // {
397 __unstable__.profiledObRun = let
398 profiled = projectOut { inherit system; enableLibraryProfiling = true; };
399 exeSource = builtins.toFile "ob-run.hs" ''
400 module Main where
402 import Control.Exception
403 import Reflex.Profiled
404 import System.Environment
406 import qualified Obelisk.Run
407 import qualified Frontend
408 import qualified Backend
410 main :: IO ()
411 main = do
412 args <- getArgs
413 let port = read $ args !! 0
414 assets = args !! 1
415 profileFile = (args !! 2) <> ".rprof"
416 Obelisk.Run.run port (Obelisk.Run.runServeAsset assets) Backend.backend Frontend.frontend `finally` writeProfilingData profileFile
417 '';
418 in nixpkgs.runCommand "ob-run" {
419 buildInputs = [ (profiled.ghc.ghcWithPackages (p: [ p.backend p.frontend])) ];
420 } ''
421 mkdir -p $out/bin/
422 ghc -x hs -prof -fno-prof-auto -threaded ${exeSource} -o $out/bin/ob-run
423 '';
425 linuxExeConfigurable = linuxExe;
426 linuxExe = linuxExe dummyVersion;
427 exe = serverOn mainProjectOut dummyVersion;
428 server = args@{ hostName, adminEmail, routeHost, enableHttps, version, module ? serverModules.mkBaseEc2 }:
429 server (args // { exe = linuxExe version; });
430 obelisk = import (base' + "/.obelisk/impl") {};
431 };
432 haskellPackageSets = {
433 inherit (reflex-platform) ghc ghcjs;
434 };