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