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