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