on cg-db-encoders-and-config help sign in
obelisk/default.nix
369 lines 16 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 in {
36 hnix = pkgs.haskell.lib.dontCheck (self.callCabal2nix "hnix" (pkgs.fetchFromGitHub {
37 owner = "haskell-nix";
38 repo = "hnix";
39 rev = "42afdc21da5d9e076eab57eaa42bfdde938192b8";
40 sha256 = "0psw384dx9bw2dp93xrzw8rd9amvcwgzn64jzzwby7sfspj6k349";
41 }) {});
42 # Need 8.0.2 build support
43 # PR: https://github.com/dmwit/universe/pull/33
44 universe-template = self.callCabal2nix "universe-template" (pkgs.fetchFromGitHub {
45 owner = "obsidiansystems";
46 repo = "universe";
47 rev = "6a71119bfa5db2b9990a2491c941469ff8ef5d13";
48 sha256 = "0z8smyainnlzcglv3dlx6x1n9j6d2jv48aa8f2421iayfkxg3js5";
49 } + /template) {};
50 })
52 pkgs.obeliskExecutableConfig.haskellOverlay
54 # Add obelisk packages
55 (self: super: let
56 pkgs = self.callPackage ({ pkgs }: pkgs) {};
57 in {
58 obelisk-executable-config-inject = pkgs.obeliskExecutableConfig.platforms.web.inject self;
60 obelisk-asset-manifest = self.callCabal2nix "obelisk-asset-manifest" (hackGet ./lib/asset + "/manifest") {};
61 obelisk-asset-serve-snap = self.callCabal2nix "obelisk-asset-serve-snap" (hackGet ./lib/asset + "/serve-snap") {};
62 obelisk-backend = self.callCabal2nix "obelisk-backend" (cleanSource ./lib/backend) {};
63 obelisk-cliapp = self.callCabal2nix "obelisk-cliapp" (cleanSource ./lib/cliapp) {};
64 obelisk-command = self.callCabal2nix "obelisk-command" (cleanSource ./lib/command) {};
65 obelisk-frontend = self.callCabal2nix "obelisk-frontend" (cleanSource ./lib/frontend) {};
66 obelisk-run = self.callCabal2nix "obelisk-run" (cleanSource ./lib/run) {};
67 obelisk-route = self.callCabal2nix "obelisk-route" (cleanSource ./lib/route) {};
68 obelisk-selftest = self.callCabal2nix "obelisk-selftest" (cleanSource ./lib/selftest) {};
69 obelisk-snap-extras = self.callCabal2nix "obelisk-snap-extras" (cleanSource ./lib/snap-extras) {};
70 tabulation = self.callCabal2nix "tabulation" (cleanSource ./lib/tabulation) {};
71 })
73 (self: super: let
74 pkgs = self.callPackage ({ pkgs }: pkgs) {};
75 haskellLib = pkgs.haskell.lib;
76 #TODO: Upstream
77 # Modify a Haskell package to add completion scripts for the given
78 # executable produced by it. These completion scripts will be picked up
79 # automatically if the resulting derivation is installed, e.g. by
80 # `nix-env -i`.
81 addOptparseApplicativeCompletionScripts = exeName: pkg: haskellLib.overrideCabal pkg (drv: {
82 postInstall = (drv.postInstall or "") + ''
83 BASH_COMP_DIR="$out/share/bash-completion/completions"
84 mkdir -p "$BASH_COMP_DIR"
85 "$out/bin/${exeName}" --bash-completion-script "$out/bin/${exeName}" >"$BASH_COMP_DIR/ob"
87 ZSH_COMP_DIR="$out/share/zsh/vendor-completions"
88 mkdir -p "$ZSH_COMP_DIR"
89 "$out/bin/${exeName}" --zsh-completion-script "$out/bin/${exeName}" >"$ZSH_COMP_DIR/_ob"
91 FISH_COMP_DIR="$out/share/fish/vendor_completions.d"
92 mkdir -p "$FISH_COMP_DIR"
93 "$out/bin/${exeName}" --fish-completion-script "$out/bin/${exeName}" >"$FISH_COMP_DIR/ob.fish"
94 '';
95 });
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 (addOptparseApplicativeCompletionScripts "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 reflex-platform = getReflexPlatform system;
117 inherit (reflex-platform) hackGet nixpkgs;
118 pkgs = nixpkgs;
120 # The haskell environment used to build Obelisk itself, e.g. the 'ob' command
121 ghcObelisk = reflex-platform.ghc;
123 # Development environments for obelisk packages.
124 ghcObeliskEnvs = pkgs.lib.mapAttrs (n: v: reflex-platform.workOn ghcObelisk v) ghcObelisk;
126 inherit (import ./lib/asset/assets.nix { inherit nixpkgs; }) mkAssets;
128 haskellLib = pkgs.haskell.lib;
130in rec {
131 inherit reflex-platform;
132 inherit (reflex-platform) nixpkgs pinBuildInputs;
133 inherit (nixpkgs) lib;
134 pathGit = ./.; # Used in CI by the migration graph hash algorithm to correctly ignore files.
135 path = reflex-platform.filterGit ./.;
136 obelisk = ghcObelisk;
137 obeliskEnvs = ghcObeliskEnvs;
138 command = ghcObelisk.obelisk-command;
139 shell = pinBuildInputs "obelisk-shell" ([command] ++ commandRuntimeDeps pkgs) [];
141 selftest = pkgs.writeScript "selftest" ''
142 #!/usr/bin/env bash
143 set -euo pipefail
145 PATH="${command}/bin:$PATH"
146 export OBELISK_IMPL="${hackGet ./.}"
147 "${ghcObelisk.obelisk-selftest}/bin/obelisk-selftest" +RTS -N -RTS "$@"
148 '';
149 #TODO: Why can't I build ./skeleton directly as a derivation? `nix-build -E ./.` doesn't work
150 skeleton = pkgs.runCommand "skeleton" {
151 dir = builtins.filterSource (path: type: builtins.trace path (baseNameOf path != ".obelisk")) ./skeleton;
152 } ''
153 ln -s "$dir" "$out"
154 '';
155 nullIfAbsent = p: if lib.pathExists p then p else null;
156 #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.
157 processAssets = { src, packageName ? "obelisk-generated-static", moduleName ? "Obelisk.Generated.Static" }: pkgs.runCommand "asset-manifest" {
158 inherit src;
159 outputs = [ "out" "haskellManifest" "symlinked" ];
160 nativeBuildInputs = [ ghcObelisk.obelisk-asset-manifest ];
161 } ''
162 set -euo pipefail
163 touch "$out"
164 mkdir -p "$symlinked"
165 obelisk-asset-manifest-generate "$src" "$haskellManifest" ${packageName} ${moduleName} "$symlinked"
166 '';
168 compressedJs = frontend: optimizationLevel: pkgs.runCommand "compressedJs" {} ''
169 mkdir $out
170 cd $out
171 ln -s "${haskellLib.justStaticExecutables frontend}/bin/frontend.jsexe/all.js" all.unminified.js
172 ${if optimizationLevel == null then ''
173 ln -s all.unminified.js all.js
174 '' else ''
175 ${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
176 echo "//# sourceMappingURL=all.js.map" >> all.js
177 ''}
178 '';
180 serverModules = {
181 mkBaseEc2 = { hostName, routeHost, enableHttps, adminEmail, ... }: {...}: {
182 imports = [
183 (pkgs.path + /nixos/modules/virtualisation/amazon-image.nix)
184 ];
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 ec2.hvm = true;
193 };
195 mkObeliskApp =
196 { exe
197 , routeHost
198 , enableHttps
199 , name ? "backend"
200 , user ? name
201 , group ? user
202 , baseUrl ? "/"
203 , internalPort ? 8000
204 , backendArgs ? "--port=${toString internalPort}"
205 , ...
206 }: {...}: {
207 services.nginx = {
208 enable = true;
209 virtualHosts."${routeHost}" = {
210 enableACME = enableHttps;
211 forceSSL = enableHttps;
212 locations.${baseUrl} = {
213 proxyPass = "http://localhost:" + toString internalPort;
214 proxyWebsockets = true;
215 };
216 };
217 };
218 systemd.services.${name} = {
219 wantedBy = [ "multi-user.target" ];
220 after = [ "network.target" ];
221 restartIfChanged = true;
222 script = ''
223 ln -sft . '${exe}'/*
224 mkdir -p log
225 exec ./backend ${backendArgs} >>backend.out 2>>backend.err </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 "${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 }@args:
259 let
260 nixos = import (pkgs.path + /nixos);
261 in nixos {
262 system = "x86_64-linux";
263 configuration = {
264 imports = [
265 (serverModules.mkBaseEc2 args)
266 (serverModules.mkObeliskApp args)
267 ];
268 };
269 };
272 # An Obelisk project is a reflex-platform project with a predefined layout and role for each component
273 project = base: projectDefinition:
274 let projectOut = sys: (getReflexPlatform sys).project (args@{ nixpkgs, ... }:
275 let mkProject = { android ? null #TODO: Better error when missing
276 , ios ? null #TODO: Better error when missing
277 , packages ? {}
278 , overrides ? _: _: {}
279 , staticFiles ? base + /static
280 , tools ? _: []
281 , shellToolOverrides ? _: _: {}
282 , withHoogle ? false # Setting this to `true` makes shell reloading far slower
283 , __closureCompilerOptimizationLevel ? "ADVANCED" # Set this to `null` to skip the closure-compiler step
284 }:
285 let frontendName = "frontend";
286 backendName = "backend";
287 commonName = "common";
288 staticName = "obelisk-generated-static";
289 staticFilesImpure = if lib.isDerivation staticFiles then staticFiles else toString staticFiles;
290 processedStatic = processAssets { src = staticFiles; };
291 # The packages whose names and roles are defined by this package
292 predefinedPackages = lib.filterAttrs (_: x: x != null) {
293 ${frontendName} = nullIfAbsent (base + "/frontend");
294 ${commonName} = nullIfAbsent (base + "/common");
295 ${backendName} = nullIfAbsent (base + "/backend");
296 };
297 combinedPackages = predefinedPackages // packages;
298 projectOverrides = self: super: {
299 ${staticName} = haskellLib.dontHaddock (self.callCabal2nix staticName processedStatic.haskellManifest {});
300 ${backendName} = haskellLib.addBuildDepend super.${backendName} self.obelisk-run;
301 };
302 totalOverrides = lib.composeExtensions projectOverrides overrides;
303 inherit (lib.strings) hasPrefix;
304 privateConfigDirs = ["config/backend"];
305 injectableConfig = builtins.filterSource (path: _:
306 !(lib.lists.any (x: hasPrefix (toString base + "/" + toString x) (toString path)) privateConfigDirs)
307 );
308 __androidWithConfig = configPath: {
309 ${if android == null then null else frontendName} = {
310 executableName = "frontend";
311 ${if builtins.pathExists staticFiles then "assets" else null} =
312 nixpkgs.obeliskExecutableConfig.platforms.android.inject
313 (injectableConfig configPath)
314 processedStatic.symlinked;
315 } // android;
316 };
317 __iosWithConfig = configPath: {
318 ${if ios == null then null else frontendName} = {
319 executableName = "frontend";
320 ${if builtins.pathExists staticFiles then "staticSrc" else null} =
321 nixpkgs.obeliskExecutableConfig.platforms.ios.inject
322 (injectableConfig configPath)
323 processedStatic.symlinked;
324 } // ios;
325 };
326 in {
327 inherit shellToolOverrides tools withHoogle;
328 overrides = totalOverrides;
329 packages = combinedPackages;
330 shells = {
331 ${if android == null && ios == null then null else "ghcSavedSplices"} = (lib.filter (x: lib.hasAttr x combinedPackages) [
332 commonName
333 frontendName
334 ]);
335 ghc = (lib.filter (x: lib.hasAttr x combinedPackages) [
336 backendName
337 commonName
338 frontendName
339 ]);
340 ghcjs = lib.filter (x: lib.hasAttr x combinedPackages) [
341 frontendName
342 commonName
343 ];
344 };
345 android = __androidWithConfig (base + "/config");
346 ios = __iosWithConfig (base + "/config");
347 passthru = { inherit android ios packages overrides tools shellToolOverrides withHoogle staticFiles staticFilesImpure __closureCompilerOptimizationLevel processedStatic __iosWithConfig __androidWithConfig; };
348 };
349 in mkProject (projectDefinition args));
350 serverOn = sys: version: serverExe
351 (projectOut sys).ghc.backend
352 (projectOut system).ghcjs.frontend
353 (projectOut sys).passthru.staticFiles
354 (projectOut sys).passthru.__closureCompilerOptimizationLevel
355 version;
356 linuxExe = serverOn "x86_64-linux";
357 dummyVersion = "Version number is only available for deployments";
358 in projectOut system // {
359 linuxExeConfigurable = linuxExe;
360 linuxExe = linuxExe dummyVersion;
361 exe = serverOn system dummyVersion;
362 server = args@{ hostName, adminEmail, routeHost, enableHttps, version }:
363 server (args // { exe = linuxExe version; });
364 obelisk = import (base + "/.obelisk/impl") {};
365 };
366 haskellPackageSets = {
367 inherit (reflex-platform) ghc ghcjs;
368 };