on eac@fix-nested-ob-filter help sign in
obelisk/default.nix
389 lines 17 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 cleanSource = builtins.filterSource (name: _: let baseName = builtins.baseNameOf name; in !(
9 builtins.match "^\\.ghc\\.environment.*" baseName != null ||
10 baseName == "cabal.project.local"
11 ));
13 commandRuntimeDeps = pkgs: with pkgs; [
14 coreutils
15 git
16 nix-prefetch-git
17 openssh
18 ];
20 getReflexPlatform = sys: reflex-platform-func {
21 inherit iosSdkVersion config;
22 system = sys;
23 enableLibraryProfiling = profiling;
25 nixpkgsOverlays = [
26 (self: super: {
27 obeliskExecutableConfig = import ./lib/executable-config { nixpkgs = pkgs; filterGitSource = cleanSource; };
28 })
29 ];
31 haskellOverlays = [
33 # Fix misc upstream packages
34 (self: super: let
35 pkgs = self.callPackage ({ pkgs }: pkgs) {};
36 haskellLib = pkgs.haskell.lib;
37 in {
38 hnix = haskellLib.dontCheck (haskellLib.doJailbreak (self.callCabal2nix "hnix" (hackGet dep/hnix) {}));
39 hnix-store-core = self.callHackage "hnix-store-core" "0.1.0.0" {};
41 ghcid = self.callCabal2nix "ghcid" (hackGet ./dep/ghcid) {};
42 })
44 pkgs.obeliskExecutableConfig.haskellOverlay
46 # Add obelisk packages
47 (self: super: let
48 pkgs = self.callPackage ({ pkgs }: pkgs) {};
49 onLinux = pkg: f: if pkgs.stdenv.isLinux then f pkg else pkg;
50 in {
51 shelly = self.callHackage "shelly" "1.9.0" {};
53 obelisk-executable-config-inject = pkgs.obeliskExecutableConfig.platforms.web.inject self;
55 obelisk-asset-manifest = self.callCabal2nix "obelisk-asset-manifest" (cleanSource ./lib/asset/manifest) {};
56 obelisk-asset-serve-snap = self.callCabal2nix "obelisk-asset-serve-snap" (cleanSource ./lib/asset/serve-snap) {};
57 obelisk-backend = self.callCabal2nix "obelisk-backend" (cleanSource ./lib/backend) {};
58 obelisk-cliapp = self.callCabal2nix "obelisk-cliapp" (cleanSource ./lib/cliapp) {};
59 obelisk-command = haskellLib.overrideCabal (self.callCabal2nix "obelisk-command" (cleanSource ./lib/command) {}) {
60 librarySystemDepends = [
61 pkgs.nix
62 (haskellLib.justStaticExecutables self.ghcid)
63 ];
64 };
65 obelisk-frontend = self.callCabal2nix "obelisk-frontend" (cleanSource ./lib/frontend) {};
66 obelisk-run = onLinux (self.callCabal2nix "obelisk-run" (cleanSource ./lib/run) {}) (pkg:
67 haskellLib.overrideCabal pkg (drv: { librarySystemDepends = [ pkgs.iproute ]; })
68 );
69 obelisk-route = self.callCabal2nix "obelisk-route" (cleanSource ./lib/route) {};
70 obelisk-selftest = haskellLib.overrideCabal (self.callCabal2nix "obelisk-selftest" (cleanSource ./lib/selftest) {}) {
71 librarySystemDepends = [
72 pkgs.cabal-install
73 pkgs.coreutils
74 pkgs.git
75 pkgs.nix
76 ];
77 };
78 obelisk-snap-extras = self.callCabal2nix "obelisk-snap-extras" (cleanSource ./lib/snap-extras) {};
79 tabulation = self.callCabal2nix "tabulation" (cleanSource ./lib/tabulation) {};
80 })
82 (self: super: let
83 pkgs = self.callPackage ({ pkgs }: pkgs) {};
84 haskellLib = pkgs.haskell.lib;
85 in {
86 # Dynamic linking with split objects dramatically increases startup time (about
87 # 0.5 seconds on a decent machine with SSD), so we do `justStaticExecutables`.
88 obelisk-command = haskellLib.overrideCabal
89 (haskellLib.generateOptparseApplicativeCompletion "ob"
90 (haskellLib.justStaticExecutables super.obelisk-command))
91 (drv: {
92 buildTools = (drv.buildTools or []) ++ [ pkgs.buildPackages.makeWrapper ];
93 postFixup = ''
94 ${drv.postFixup or ""}
95 # Make `ob` reference its runtime dependencies.
96 wrapProgram "$out"/bin/ob --prefix PATH : ${pkgs.lib.makeBinPath (commandRuntimeDeps pkgs)}
97 '';
98 });
99 obelisk-selftest = haskellLib.justStaticExecutables super.obelisk-selftest;
100 })
102 ];
103 };
105 reflex-platform = getReflexPlatform system;
106 inherit (reflex-platform) hackGet nixpkgs;
107 pkgs = nixpkgs;
109 # The haskell environment used to build Obelisk itself, e.g. the 'ob' command
110 ghcObelisk = reflex-platform.ghc;
112 # Development environments for obelisk packages.
113 ghcObeliskEnvs = pkgs.lib.mapAttrs (n: v: reflex-platform.workOn ghcObelisk v) ghcObelisk;
115 inherit (import ./lib/asset/assets.nix { inherit nixpkgs; }) mkAssets;
117 haskellLib = pkgs.haskell.lib;
119in rec {
120 inherit reflex-platform;
121 inherit (reflex-platform) nixpkgs pinBuildInputs;
122 inherit (nixpkgs) lib;
123 pathGit = ./.; # Used in CI by the migration graph hash algorithm to correctly ignore files.
124 path = reflex-platform.filterGit ./.;
125 obelisk = ghcObelisk;
126 obeliskEnvs = pkgs.lib.filterAttrs (k: _: pkgs.lib.strings.hasPrefix "obelisk-" k) ghcObeliskEnvs;
127 command = ghcObelisk.obelisk-command;
128 shell = pinBuildInputs "obelisk-shell" ([command] ++ commandRuntimeDeps pkgs);
130 selftest = pkgs.writeScript "selftest" ''
131 #!${pkgs.runtimeShell}
132 set -euo pipefail
134 PATH="${command}/bin:$PATH"
135 cd ${./.}
136 "${ghcObelisk.obelisk-selftest}/bin/obelisk-selftest" +RTS -N -RTS "$@"
137 '';
138 skeleton = pkgs.runCommand "skeleton" {
139 dir = builtins.filterSource (path: type: builtins.trace path (baseNameOf path != ".obelisk")) ./skeleton;
140 } ''
141 ln -s "$dir" "$out"
142 '';
143 nullIfAbsent = p: if lib.pathExists p then p else null;
144 #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.
145 processAssets = { src, packageName ? "obelisk-generated-static", moduleName ? "Obelisk.Generated.Static" }: pkgs.runCommand "asset-manifest" {
146 inherit src;
147 outputs = [ "out" "haskellManifest" "symlinked" ];
148 nativeBuildInputs = [ ghcObelisk.obelisk-asset-manifest ];
149 } ''
150 set -euo pipefail
151 touch "$out"
152 mkdir -p "$symlinked"
153 obelisk-asset-manifest-generate "$src" "$haskellManifest" ${packageName} ${moduleName} "$symlinked"
154 '';
156 compressedJs = frontend: optimizationLevel: pkgs.runCommand "compressedJs" {} ''
157 mkdir $out
158 cd $out
159 # TODO profiling + static shouldn't break and need an ad-hoc workaround like that
160 ln -s "${haskellLib.justStaticExecutables frontend}/bin/frontend.jsexe/all.js" all.unminified.js
161 ${if optimizationLevel == null then ''
162 ln -s all.unminified.js all.js
163 '' else ''
164 ${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
165 echo "//# sourceMappingURL=all.js.map" >> all.js
166 ''}
167 '';
169 serverModules = {
170 mkBaseEc2 = { hostName, routeHost, enableHttps, adminEmail, ... }: {...}: {
171 imports = [
172 (pkgs.path + /nixos/modules/virtualisation/amazon-image.nix)
173 ];
174 networking = {
175 inherit hostName;
176 firewall.allowedTCPPorts = if enableHttps then [ 80 443 ] else [ 80 ];
177 };
178 security.acme.certs = if enableHttps then {
179 "${routeHost}".email = adminEmail;
180 } else {};
181 ec2.hvm = true;
182 };
184 mkObeliskApp =
185 { exe
186 , routeHost
187 , enableHttps
188 , name ? "backend"
189 , user ? name
190 , group ? user
191 , baseUrl ? "/"
192 , internalPort ? 8000
193 , backendArgs ? "--port=${toString internalPort}"
194 , ...
195 }: {...}: {
196 services.nginx = {
197 enable = true;
198 virtualHosts."${routeHost}" = {
199 enableACME = enableHttps;
200 forceSSL = enableHttps;
201 locations.${baseUrl} = {
202 proxyPass = "http://127.0.0.1:" + toString internalPort;
203 proxyWebsockets = true;
204 };
205 };
206 };
207 systemd.services.${name} = {
208 wantedBy = [ "multi-user.target" ];
209 after = [ "network.target" ];
210 restartIfChanged = true;
211 path = [ pkgs.gnutar ];
212 script = ''
213 ln -sft . '${exe}'/*
214 mkdir -p log
215 exec ./backend ${backendArgs} </dev/null
216 '';
217 serviceConfig = {
218 User = user;
219 KillMode = "process";
220 WorkingDirectory = "~";
221 Restart = "always";
222 RestartSec = 5;
223 };
224 };
225 users = {
226 users.${user} = {
227 description = "${user} service";
228 home = "/var/lib/${user}";
229 createHome = true;
230 isSystemUser = true;
231 group = group;
232 };
233 groups.${group} = {};
234 };
235 };
236 };
238 serverExe = backend: frontend: assets: optimizationLevel: version:
239 pkgs.runCommand "serverExe" {} ''
240 mkdir $out
241 set -eux
242 ln -s "${if profiling then backend else haskellLib.justStaticExecutables backend}"/bin/* $out/
243 ln -s "${mkAssets assets}" $out/static.assets
244 ln -s ${mkAssets (compressedJs frontend optimizationLevel)} $out/frontend.jsexe.assets
245 echo ${version} > $out/version
246 '';
248 server = { exe, hostName, adminEmail, routeHost, enableHttps, version }@args:
249 let
250 nixos = import (pkgs.path + /nixos);
251 in nixos {
252 system = "x86_64-linux";
253 configuration = {
254 imports = [
255 (serverModules.mkBaseEc2 args)
256 (serverModules.mkObeliskApp args)
257 ];
258 };
259 };
261 # An Obelisk project is a reflex-platform project with a predefined layout and role for each component
262 project = base': projectDefinition:
263 let
264 projectOut = sys: let reflexPlatformProject = (getReflexPlatform sys).project; in reflexPlatformProject (args@{ nixpkgs, ... }:
265 let
266 inherit (lib.strings) hasPrefix;
267 mkProject =
268 { android ? null #TODO: Better error when missing
269 , ios ? null #TODO: Better error when missing
270 , packages ? {}
271 , overrides ? _: _: {}
272 , staticFiles ? null
273 , tools ? _: []
274 , shellToolOverrides ? _: _: {}
275 , withHoogle ? false # Setting this to `true` makes shell reloading far slower
276 , __closureCompilerOptimizationLevel ? "ADVANCED" # Set this to `null` to skip the closure-compiler step
277 }:
278 let
279 allConfig = nixpkgs.lib.makeExtensible (self: {
280 base = base';
281 inherit args;
282 userSettings = {
283 inherit android ios packages overrides tools shellToolOverrides withHoogle __closureCompilerOptimizationLevel;
284 staticFiles = if staticFiles == null then self.base + /static else staticFiles;
285 };
286 frontendName = "frontend";
287 backendName = "backend";
288 commonName = "common";
289 staticName = "obelisk-generated-static";
290 staticFilesImpure = let fs = self.userSettings.staticFiles; in if lib.isDerivation fs then fs else toString fs;
291 processedStatic = processAssets { src = self.userSettings.staticFiles; };
292 # The packages whose names and roles are defined by this package
293 predefinedPackages = lib.filterAttrs (_: x: x != null) {
294 ${self.frontendName} = nullIfAbsent (self.base + "/frontend");
295 ${self.commonName} = nullIfAbsent (self.base + "/common");
296 ${self.backendName} = nullIfAbsent (self.base + "/backend");
297 };
298 shellPackages = {};
299 combinedPackages = self.predefinedPackages // self.userSettings.packages // self.shellPackages;
300 projectOverrides = self': super': {
301 ${self.staticName} = haskellLib.dontHaddock (self'.callCabal2nix self.staticName self.processedStatic.haskellManifest {});
302 ${self.backendName} = haskellLib.addBuildDepend super'.${self.backendName} self'.obelisk-run;
303 };
304 totalOverrides = lib.composeExtensions self.projectOverrides self.userSettings.overrides;
305 privateConfigDirs = ["config/backend"];
306 injectableConfig = builtins.filterSource (path: _:
307 !(lib.lists.any (x: hasPrefix (toString self.base + "/" + toString x) (toString path)) self.privateConfigDirs)
308 );
309 __androidWithConfig = configPath: {
310 ${if self.userSettings.android == null then null else self.frontendName} = {
311 executableName = "frontend";
312 ${if builtins.pathExists self.userSettings.staticFiles then "assets" else null} =
313 nixpkgs.obeliskExecutableConfig.platforms.android.inject
314 (self.injectableConfig configPath)
315 self.processedStatic.symlinked;
316 } // self.userSettings.android;
317 };
318 __iosWithConfig = configPath: {
319 ${if self.userSettings.ios == null then null else self.frontendName} = {
320 executableName = "frontend";
321 ${if builtins.pathExists self.userSettings.staticFiles then "staticSrc" else null} =
322 nixpkgs.obeliskExecutableConfig.platforms.ios.inject
323 (self.injectableConfig configPath)
324 self.processedStatic.symlinked;
325 } // self.userSettings.ios;
326 };
328 shells-ghc = builtins.attrNames (self.predefinedPackages // self.shellPackages);
330 shells-ghcjs = [
331 self.frontendName
332 self.commonName
333 ];
335 shells-ghcSavedSplices = [
336 self.commonName
337 self.frontendName
338 ];
340 project = reflexPlatformProject ({...}: self.projectConfig);
341 projectConfig = {
342 inherit (self.userSettings) shellToolOverrides tools withHoogle;
343 overrides = self.totalOverrides;
344 packages = self.combinedPackages;
345 shells = {
346 ${if self.userSettings.android == null && self.userSettings.ios == null then null else "ghcSavedSplices"} =
347 lib.filter (x: lib.hasAttr x self.combinedPackages) self.shells-ghcSavedSplices;
348 ghc = lib.filter (x: lib.hasAttr x self.combinedPackages) self.shells-ghc;
349 ghcjs = lib.filter (x: lib.hasAttr x self.combinedPackages) self.shells-ghcjs;
350 };
351 android = self.__androidWithConfig (self.base + "/config");
352 ios = self.__iosWithConfig (self.base + "/config");
354 passthru = {
355 __unstable__.self = allConfig;
356 inherit (self)
357 staticFilesImpure processedStatic
358 __iosWithConfig __androidWithConfig
359 ;
360 inherit (self.userSettings)
361 android ios overrides packages shellToolOverrides staticFiles tools withHoogle
362 __closureCompilerOptimizationLevel
363 ;
364 };
365 };
366 });
367 in allConfig;
368 in (mkProject (projectDefinition args)).projectConfig);
369 mainProjectOut = projectOut system;
370 serverOn = projectInst: version: serverExe
371 projectInst.ghc.backend
372 mainProjectOut.ghcjs.frontend
373 projectInst.passthru.staticFiles
374 projectInst.passthru.__closureCompilerOptimizationLevel
375 version;
376 linuxExe = serverOn (projectOut "x86_64-linux");
377 dummyVersion = "Version number is only available for deployments";
378 in mainProjectOut // {
379 linuxExeConfigurable = linuxExe;
380 linuxExe = linuxExe dummyVersion;
381 exe = serverOn mainProjectOut dummyVersion;
382 server = args@{ hostName, adminEmail, routeHost, enableHttps, version }:
383 server (args // { exe = linuxExe version; });
384 obelisk = import (base' + "/.obelisk/impl") {};
385 };
386 haskellPackageSets = {
387 inherit (reflex-platform) ghc ghcjs;
388 };