on easier-r-slash-import help sign in
obelisk/default.nix
359 lines 15 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 ];
75 };
76 obelisk-snap-extras = self.callCabal2nix "obelisk-snap-extras" (cleanSource ./lib/snap-extras) {};
77 tabulation = self.callCabal2nix "tabulation" (cleanSource ./lib/tabulation) {};
78 })
80 (self: super: let
81 pkgs = self.callPackage ({ pkgs }: pkgs) {};
82 haskellLib = pkgs.haskell.lib;
83 in {
84 # Dynamic linking with split objects dramatically increases startup time (about
85 # 0.5 seconds on a decent machine with SSD), so we do `justStaticExecutables`.
86 obelisk-command = haskellLib.overrideCabal
87 (haskellLib.generateOptparseApplicativeCompletion "ob"
88 (haskellLib.justStaticExecutables super.obelisk-command))
89 (drv: {
90 buildTools = (drv.buildTools or []) ++ [ pkgs.buildPackages.makeWrapper ];
91 postFixup = ''
92 ${drv.postFixup or ""}
93 # Make `ob` reference its runtime dependencies.
94 wrapProgram "$out"/bin/ob --prefix PATH : ${pkgs.lib.makeBinPath (commandRuntimeDeps pkgs)}
95 '';
96 });
97 obelisk-selftest = haskellLib.justStaticExecutables super.obelisk-selftest;
98 })
100 ];
101 };
103 reflex-platform = getReflexPlatform system;
104 inherit (reflex-platform) hackGet nixpkgs;
105 pkgs = nixpkgs;
107 # The haskell environment used to build Obelisk itself, e.g. the 'ob' command
108 ghcObelisk = reflex-platform.ghc;
110 # Development environments for obelisk packages.
111 ghcObeliskEnvs = pkgs.lib.mapAttrs (n: v: reflex-platform.workOn ghcObelisk v) ghcObelisk;
113 inherit (import ./lib/asset/assets.nix { inherit nixpkgs; }) mkAssets;
115 haskellLib = pkgs.haskell.lib;
117in rec {
118 inherit reflex-platform;
119 inherit (reflex-platform) nixpkgs pinBuildInputs;
120 inherit (nixpkgs) lib;
121 pathGit = ./.; # Used in CI by the migration graph hash algorithm to correctly ignore files.
122 path = reflex-platform.filterGit ./.;
123 obelisk = ghcObelisk;
124 obeliskEnvs = pkgs.lib.filterAttrs (k: _: pkgs.lib.strings.hasPrefix "obelisk-" k) ghcObeliskEnvs;
125 command = ghcObelisk.obelisk-command;
126 shell = pinBuildInputs "obelisk-shell" ([command] ++ commandRuntimeDeps pkgs);
128 selftest = pkgs.writeScript "selftest" ''
129 #!/usr/bin/env bash
130 set -euo pipefail
132 PATH="${command}/bin:$PATH"
133 export OBELISK_IMPL="$(mktemp -d)"
134 git clone file://${pathGit} $OBELISK_IMPL
135 "${ghcObelisk.obelisk-selftest}/bin/obelisk-selftest" +RTS -N -RTS "$@"
136 '';
137 skeleton = pkgs.runCommand "skeleton" {
138 dir = builtins.filterSource (path: type: builtins.trace path (baseNameOf path != ".obelisk")) ./skeleton;
139 } ''
140 ln -s "$dir" "$out"
141 '';
142 nullIfAbsent = p: if lib.pathExists p then p else null;
143 #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.
144 processAssets = { src, packageName ? "obelisk-generated-static", moduleName ? "Obelisk.Generated.Static" }: pkgs.runCommand "asset-manifest" {
145 inherit src;
146 outputs = [ "out" "haskellManifest" "symlinked" ];
147 nativeBuildInputs = [ ghcObelisk.obelisk-asset-manifest ];
148 } ''
149 set -euo pipefail
150 touch "$out"
151 mkdir -p "$symlinked"
152 obelisk-asset-manifest-generate "$src" "$haskellManifest" ${packageName} ${moduleName} "$symlinked"
153 '';
155 compressedJs = frontend: optimizationLevel: pkgs.runCommand "compressedJs" {} ''
156 mkdir $out
157 cd $out
158 # TODO profiling + static shouldn't break and need an ad-hoc workaround like that
159 ln -s "${haskellLib.justStaticExecutables frontend}/bin/frontend.jsexe/all.js" all.unminified.js
160 ${if optimizationLevel == null then ''
161 ln -s all.unminified.js all.js
162 '' else ''
163 ${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
164 echo "//# sourceMappingURL=all.js.map" >> all.js
165 ''}
166 '';
168 serverModules = {
169 mkBaseEc2 = { hostName, routeHost, enableHttps, adminEmail, ... }: {...}: {
170 imports = [
171 (pkgs.path + /nixos/modules/virtualisation/amazon-image.nix)
172 ];
173 networking = {
174 inherit hostName;
175 firewall.allowedTCPPorts = if enableHttps then [ 80 443 ] else [ 80 ];
176 };
177 security.acme.certs = if enableHttps then {
178 "${routeHost}".email = adminEmail;
179 } else {};
180 ec2.hvm = true;
181 };
183 mkObeliskApp =
184 { exe
185 , routeHost
186 , enableHttps
187 , name ? "backend"
188 , user ? name
189 , group ? user
190 , baseUrl ? "/"
191 , internalPort ? 8000
192 , backendArgs ? "--port=${toString internalPort}"
193 , ...
194 }: {...}: {
195 services.nginx = {
196 enable = true;
197 virtualHosts."${routeHost}" = {
198 enableACME = enableHttps;
199 forceSSL = enableHttps;
200 locations.${baseUrl} = {
201 proxyPass = "http://127.0.0.1:" + toString internalPort;
202 proxyWebsockets = true;
203 };
204 };
205 };
206 systemd.services.${name} = {
207 wantedBy = [ "multi-user.target" ];
208 after = [ "network.target" ];
209 restartIfChanged = true;
210 path = [ pkgs.gnutar ];
211 script = ''
212 ln -sft . '${exe}'/*
213 mkdir -p log
214 exec ./backend ${backendArgs} </dev/null
215 '';
216 serviceConfig = {
217 User = user;
218 KillMode = "process";
219 WorkingDirectory = "~";
220 Restart = "always";
221 RestartSec = 5;
222 };
223 };
224 users = {
225 users.${user} = {
226 description = "${user} service";
227 home = "/var/lib/${user}";
228 createHome = true;
229 isSystemUser = true;
230 group = group;
231 };
232 groups.${group} = {};
233 };
234 };
235 };
237 serverExe = backend: frontend: assets: optimizationLevel: version:
238 pkgs.runCommand "serverExe" {} ''
239 mkdir $out
240 set -eux
241 ln -s "${if profiling then backend else haskellLib.justStaticExecutables backend}"/bin/* $out/
242 ln -s "${mkAssets assets}" $out/static.assets
243 ln -s ${mkAssets (compressedJs frontend optimizationLevel)} $out/frontend.jsexe.assets
244 echo ${version} > $out/version
245 '';
247 server = { exe, hostName, adminEmail, routeHost, enableHttps, version }@args:
248 let
249 nixos = import (pkgs.path + /nixos);
250 in nixos {
251 system = "x86_64-linux";
252 configuration = {
253 imports = [
254 (serverModules.mkBaseEc2 args)
255 (serverModules.mkObeliskApp args)
256 ];
257 };
258 };
261 # An Obelisk project is a reflex-platform project with a predefined layout and role for each component
262 project = base: projectDefinition:
263 let projectOut = sys: (getReflexPlatform sys).project (args@{ nixpkgs, ... }:
264 let mkProject = { android ? null #TODO: Better error when missing
265 , ios ? null #TODO: Better error when missing
266 , packages ? {}
267 , overrides ? _: _: {}
268 , staticFiles ? base + /static
269 , tools ? _: []
270 , shellToolOverrides ? _: _: {}
271 , withHoogle ? false # Setting this to `true` makes shell reloading far slower
272 , __closureCompilerOptimizationLevel ? "ADVANCED" # Set this to `null` to skip the closure-compiler step
273 }:
274 let frontendName = "frontend";
275 backendName = "backend";
276 commonName = "common";
277 staticName = "obelisk-generated-static";
278 staticFilesImpure = if lib.isDerivation staticFiles then staticFiles else toString staticFiles;
279 processedStatic = processAssets { src = staticFiles; };
280 # The packages whose names and roles are defined by this package
281 predefinedPackages = lib.filterAttrs (_: x: x != null) {
282 ${frontendName} = nullIfAbsent (base + "/frontend");
283 ${commonName} = nullIfAbsent (base + "/common");
284 ${backendName} = nullIfAbsent (base + "/backend");
285 };
286 combinedPackages = predefinedPackages // packages;
287 projectOverrides = self: super: {
288 ${staticName} = haskellLib.dontHaddock (self.callCabal2nix staticName processedStatic.haskellManifest {});
289 ${backendName} = haskellLib.addBuildDepend super.${backendName} self.obelisk-run;
290 };
291 totalOverrides = lib.composeExtensions projectOverrides overrides;
292 inherit (lib.strings) hasPrefix;
293 privateConfigDirs = ["config/backend"];
294 injectableConfig = builtins.filterSource (path: _:
295 !(lib.lists.any (x: hasPrefix (toString base + "/" + toString x) (toString path)) privateConfigDirs)
296 );
297 __androidWithConfig = configPath: {
298 ${if android == null then null else frontendName} = {
299 executableName = "frontend";
300 ${if builtins.pathExists staticFiles then "assets" else null} =
301 nixpkgs.obeliskExecutableConfig.platforms.android.inject
302 (injectableConfig configPath)
303 processedStatic.symlinked;
304 } // android;
305 };
306 __iosWithConfig = configPath: {
307 ${if ios == null then null else frontendName} = {
308 executableName = "frontend";
309 ${if builtins.pathExists staticFiles then "staticSrc" else null} =
310 nixpkgs.obeliskExecutableConfig.platforms.ios.inject
311 (injectableConfig configPath)
312 processedStatic.symlinked;
313 } // ios;
314 };
315 in {
316 inherit shellToolOverrides tools withHoogle;
317 overrides = totalOverrides;
318 packages = combinedPackages;
319 shells = {
320 ${if android == null && ios == null then null else "ghcSavedSplices"} = (lib.filter (x: lib.hasAttr x combinedPackages) [
321 commonName
322 frontendName
323 ]);
324 ghc = (lib.filter (x: lib.hasAttr x combinedPackages) [
325 backendName
326 commonName
327 frontendName
328 ]);
329 ghcjs = lib.filter (x: lib.hasAttr x combinedPackages) [
330 frontendName
331 commonName
332 ];
333 };
334 android = __androidWithConfig (base + "/config");
335 ios = __iosWithConfig (base + "/config");
336 passthru = { inherit android ios packages overrides tools shellToolOverrides withHoogle staticFiles staticFilesImpure __closureCompilerOptimizationLevel processedStatic __iosWithConfig __androidWithConfig; };
337 };
338 in mkProject (projectDefinition args));
339 mainProjectOut = projectOut system;
340 serverOn = projectInst: version: serverExe
341 projectInst.ghc.backend
342 mainProjectOut.ghcjs.frontend
343 projectInst.passthru.staticFiles
344 projectInst.passthru.__closureCompilerOptimizationLevel
345 version;
346 linuxExe = serverOn (projectOut "x86_64-linux");
347 dummyVersion = "Version number is only available for deployments";
348 in mainProjectOut // {
349 linuxExeConfigurable = linuxExe;
350 linuxExe = linuxExe dummyVersion;
351 exe = serverOn mainProjectOut dummyVersion;
352 server = args@{ hostName, adminEmail, routeHost, enableHttps, version }:
353 server (args // { exe = linuxExe version; });
354 obelisk = import (base + "/.obelisk/impl") {};
355 };
356 haskellPackageSets = {
357 inherit (reflex-platform) ghc ghcjs;
358 };