on deploy-init-create-clientSessionKey help sign in
obelisk/default.nix
361 lines 16 kB history raw
1{ system ? builtins.currentSystem
2, profiling ? false
3, iosSdkVersion ? "10.2"
4, __useLegacyCompilers ? false
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 = getReflexPlatform' __useLegacyCompilers;
20 getReflexPlatform' = __useLegacyCompilers: sys: import ./dep/reflex-platform {
21 inherit iosSdkVersion __useLegacyCompilers;
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 in {
37 hnix = pkgs.haskell.lib.dontCheck (self.callCabal2nix "hnix" (pkgs.fetchFromGitHub {
38 owner = "haskell-nix";
39 repo = "hnix";
40 rev = "42afdc21da5d9e076eab57eaa42bfdde938192b8";
41 sha256 = "0psw384dx9bw2dp93xrzw8rd9amvcwgzn64jzzwby7sfspj6k349";
42 }) {});
43 # Need 8.0.2 build support
44 # PR: https://github.com/dmwit/universe/pull/33
45 universe-template = self.callCabal2nix "universe-template" (pkgs.fetchFromGitHub {
46 owner = "obsidiansystems";
47 repo = "universe";
48 rev = "6a71119bfa5db2b9990a2491c941469ff8ef5d13";
49 sha256 = "0z8smyainnlzcglv3dlx6x1n9j6d2jv48aa8f2421iayfkxg3js5";
50 } + /template) {};
51 })
53 # Add obelisk packages
54 (self: super: let
55 pkgs = self.callPackage ({ pkgs }: pkgs) {};
56 in {
57 obelisk-executable-config = pkgs.obeliskExecutableConfig.haskellPackage self;
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 })
72 (self: super: let
73 pkgs = self.callPackage ({ pkgs }: pkgs) {};
74 haskellLib = pkgs.haskell.lib;
75 #TODO: Upstream
76 # Modify a Haskell package to add completion scripts for the given
77 # executable produced by it. These completion scripts will be picked up
78 # automatically if the resulting derivation is installed, e.g. by
79 # `nix-env -i`.
80 addOptparseApplicativeCompletionScripts = exeName: pkg: haskellLib.overrideCabal pkg (drv: {
81 postInstall = (drv.postInstall or "") + ''
82 BASH_COMP_DIR="$out/share/bash-completion/completions"
83 mkdir -p "$BASH_COMP_DIR"
84 "$out/bin/${exeName}" --bash-completion-script "$out/bin/${exeName}" >"$BASH_COMP_DIR/ob"
86 ZSH_COMP_DIR="$out/share/zsh/vendor-completions"
87 mkdir -p "$ZSH_COMP_DIR"
88 "$out/bin/${exeName}" --zsh-completion-script "$out/bin/${exeName}" >"$ZSH_COMP_DIR/_ob"
90 FISH_COMP_DIR="$out/share/fish/vendor_completions.d"
91 mkdir -p "$FISH_COMP_DIR"
92 "$out/bin/${exeName}" --fish-completion-script "$out/bin/${exeName}" >"$FISH_COMP_DIR/ob.fish"
93 '';
94 });
95 in {
96 # Dynamic linking with split objects dramatically increases startup time (about
97 # 0.5 seconds on a decent machine with SSD), so we do `justStaticExecutables`.
98 obelisk-command = haskellLib.overrideCabal
99 (addOptparseApplicativeCompletionScripts "ob"
100 (haskellLib.justStaticExecutables super.obelisk-command))
101 (drv: {
102 buildTools = (drv.buildTools or []) ++ [ pkgs.buildPackages.makeWrapper ];
103 postFixup = ''
104 ${drv.postFixup or ""}
105 # Make `ob` reference its runtime dependencies.
106 wrapProgram "$out"/bin/ob --prefix PATH : ${pkgs.lib.makeBinPath (commandRuntimeDeps pkgs)}
107 '';
108 });
109 obelisk-selftest = haskellLib.justStaticExecutables super.obelisk-selftest;
110 })
112 ];
113 };
115 reflex-platform = getReflexPlatform' false system;
116 inherit (reflex-platform) hackGet nixpkgs;
117 pkgs = nixpkgs;
119 # The haskell environment used to build Obelisk itself, e.g. the 'ob' command
120 ghcObelisk = reflex-platform.ghc;
122 # Development environments for obelisk packages.
123 ghcObeliskEnvs = pkgs.lib.mapAttrs (n: v: reflex-platform.workOn ghcObelisk v) ghcObelisk;
125 inherit (import ./lib/asset/assets.nix { inherit nixpkgs; }) mkAssets;
127 haskellLib = pkgs.haskell.lib;
129in rec {
130 inherit reflex-platform;
131 inherit (reflex-platform) nixpkgs pinBuildInputs;
132 inherit (nixpkgs) lib;
133 pathGit = ./.; # Used in CI by the migration graph hash algorithm to correctly ignore files.
134 path = reflex-platform.filterGit ./.;
135 obelisk = ghcObelisk;
136 obeliskEnvs = ghcObeliskEnvs;
137 command = ghcObelisk.obelisk-command;
138 shell = pinBuildInputs "obelisk-shell" ([command] ++ commandRuntimeDeps pkgs) [];
140 selftest = pkgs.writeScript "selftest" ''
141 #!/usr/bin/env bash
142 set -euo pipefail
144 PATH="${command}/bin:$PATH"
145 export OBELISK_IMPL="${hackGet ./.}"
146 "${ghcObelisk.obelisk-selftest}/bin/obelisk-selftest" +RTS -N -RTS "$@"
147 '';
148 #TODO: Why can't I build ./skeleton directly as a derivation? `nix-build -E ./.` doesn't work
149 skeleton = pkgs.runCommand "skeleton" {
150 dir = builtins.filterSource (path: type: builtins.trace path (baseNameOf path != ".obelisk")) ./skeleton;
151 } ''
152 ln -s "$dir" "$out"
153 '';
154 nullIfAbsent = p: if lib.pathExists p then p else null;
155 #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.
156 processAssets = { src, packageName ? "obelisk-generated-static", moduleName ? "Obelisk.Generated.Static" }: pkgs.runCommand "asset-manifest" {
157 inherit src;
158 outputs = [ "out" "haskellManifest" "symlinked" ];
159 nativeBuildInputs = [ ghcObelisk.obelisk-asset-manifest ];
160 } ''
161 set -euo pipefail
162 touch "$out"
163 mkdir -p "$symlinked"
164 obelisk-asset-manifest-generate "$src" "$haskellManifest" ${packageName} ${moduleName} "$symlinked"
165 '';
167 compressedJs = frontend: optimizationLevel: pkgs.runCommand "compressedJs" { buildInputs = [ pkgs.closurecompiler ]; } ''
168 mkdir $out
169 cd $out
170 ln -s "${haskellLib.justStaticExecutables frontend}/bin/frontend.jsexe/all.js" all.unminified.js
171 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 '';
175 serverModules = {
176 mkBaseEc2 = { hostName, routeHost, enableHttps, adminEmail, ... }: {...}: {
177 imports = [
178 (pkgs.path + /nixos/modules/virtualisation/amazon-image.nix)
179 ];
180 networking = {
181 inherit hostName;
182 firewall.allowedTCPPorts = if enableHttps then [ 80 443 ] else [ 80 ];
183 };
184 security.acme.certs = if enableHttps then {
185 "${routeHost}".email = adminEmail;
186 } else {};
187 ec2.hvm = true;
188 };
190 mkObeliskApp =
191 { exe
192 , routeHost
193 , enableHttps
194 , name ? "backend"
195 , user ? name
196 , group ? user
197 , baseUrl ? "/"
198 , internalPort ? 8000
199 , backendArgs ? "--port=${toString internalPort}"
200 , ...
201 }: {...}: {
202 services.nginx = {
203 enable = true;
204 virtualHosts."${routeHost}" = {
205 enableACME = enableHttps;
206 forceSSL = enableHttps;
207 locations.${baseUrl} = {
208 proxyPass = "http://localhost:" + toString internalPort;
209 };
210 };
211 };
212 systemd.services.${name} = {
213 wantedBy = [ "multi-user.target" ];
214 after = [ "network.target" ];
215 restartIfChanged = true;
216 script = ''
217 ln -sft . '${exe}'/*
218 mkdir -p log
219 exec ./backend ${backendArgs} >>backend.out 2>>backend.err </dev/null
220 '';
221 serviceConfig = {
222 User = user;
223 KillMode = "process";
224 WorkingDirectory = "~";
225 Restart = "always";
226 RestartSec = 5;
227 };
228 };
229 users = {
230 users.${user} = {
231 description = "${user} service";
232 home = "/var/lib/${user}";
233 createHome = true;
234 isSystemUser = true;
235 group = group;
236 };
237 groups.${group} = {};
238 };
239 };
240 };
242 serverExe = backend: frontend: assets: optimizationLevel: version:
243 pkgs.runCommand "serverExe" {} ''
244 mkdir $out
245 set -eux
246 ln -s "${haskellLib.justStaticExecutables backend}"/bin/* $out/
247 ln -s "${mkAssets assets}" $out/static.assets
248 ln -s ${mkAssets (compressedJs frontend optimizationLevel)} $out/frontend.jsexe.assets
249 echo ${version} > $out/version
250 '';
252 server = { exe, hostName, adminEmail, routeHost, enableHttps, version }@args:
253 let
254 nixos = import (pkgs.path + /nixos);
255 in nixos {
256 system = "x86_64-linux";
257 configuration = {
258 imports = [
259 (serverModules.mkBaseEc2 args)
260 (serverModules.mkObeliskApp args)
261 ];
262 };
263 };
266 # An Obelisk project is a reflex-platform project with a predefined layout and role for each component
267 project = base: projectDefinition:
268 let projectOut = sys: (getReflexPlatform sys).project (args@{ nixpkgs, ... }:
269 let mkProject = { android ? null #TODO: Better error when missing
270 , ios ? null #TODO: Better error when missing
271 , packages ? {}
272 , overrides ? _: _: {}
273 , staticFiles ? base + /static
274 , tools ? _: []
275 , shellToolOverrides ? _: _: {}
276 , withHoogle ? false # Setting this to `true` makes shell reloading far slower
277 , __closureCompilerOptimizationLevel ? "ADVANCED"
278 }:
279 let frontendName = "frontend";
280 backendName = "backend";
281 commonName = "common";
282 staticName = "obelisk-generated-static";
283 staticFilesImpure = if lib.isDerivation staticFiles then staticFiles else toString staticFiles;
284 processedStatic = processAssets { src = staticFiles; };
285 # The packages whose names and roles are defined by this package
286 predefinedPackages = lib.filterAttrs (_: x: x != null) {
287 ${frontendName} = nullIfAbsent (base + "/frontend");
288 ${commonName} = nullIfAbsent (base + "/common");
289 ${backendName} = nullIfAbsent (base + "/backend");
290 };
291 combinedPackages = predefinedPackages // packages;
292 projectOverrides = self: super: {
293 ${staticName} = haskellLib.dontHaddock (self.callCabal2nix staticName processedStatic.haskellManifest {});
294 ${backendName} = haskellLib.addBuildDepend super.${backendName} self.obelisk-run;
295 };
296 totalOverrides = lib.composeExtensions projectOverrides overrides;
297 inherit (lib.strings) hasPrefix;
298 privateConfigDirs = ["config/backend"];
299 injectableConfig = builtins.filterSource (path: _:
300 !(lib.lists.any (x: hasPrefix (toString base + "/" + toString x) (toString path)) privateConfigDirs)
301 );
302 __android = configPath: {
303 ${if android == null then null else frontendName} = {
304 executableName = "frontend";
305 ${if builtins.pathExists staticFiles then "assets" else null} =
306 (if configPath == null then lib.id else nixpkgs.obeliskExecutableConfig.platforms.android.inject (injectableConfig configPath))
307 processedStatic.symlinked;
308 } // android;
309 };
310 __ios = configPath: {
311 ${if ios == null then null else frontendName} = {
312 executableName = "frontend";
313 ${if builtins.pathExists staticFiles then "staticSrc" else null} =
314 (if configPath == null then lib.id else nixpkgs.obeliskExecutableConfig.platforms.ios.inject (injectableConfig configPath))
315 processedStatic.symlinked;
316 } // ios;
317 };
318 in {
319 inherit shellToolOverrides tools withHoogle;
320 overrides = totalOverrides;
321 packages = combinedPackages;
322 shells = {
323 ${if android == null && ios == null then null else "ghcSavedSplices"} = (lib.filter (x: lib.hasAttr x combinedPackages) [
324 commonName
325 frontendName
326 ]);
327 ghc = (lib.filter (x: lib.hasAttr x combinedPackages) [
328 backendName
329 commonName
330 frontendName
331 ]);
332 ghcjs = lib.filter (x: lib.hasAttr x combinedPackages) [
333 frontendName
334 commonName
335 ];
336 };
337 android = __android null;
338 ios = __ios null;
339 passthru = { inherit android ios packages overrides tools shellToolOverrides withHoogle staticFiles staticFilesImpure __closureCompilerOptimizationLevel processedStatic __ios __android; };
340 };
341 in mkProject (projectDefinition args));
342 serverOn = sys: version: serverExe
343 (projectOut sys).ghc.backend
344 (projectOut system).ghcjs.frontend
345 (projectOut sys).passthru.staticFiles
346 (projectOut sys).passthru.__closureCompilerOptimizationLevel
347 version;
348 linuxExe = serverOn "x86_64-linux";
349 dummyVersion = "Version number is only available for deployments";
350 in projectOut system // {
351 linuxExeConfigurable = linuxExe;
352 linuxExe = linuxExe dummyVersion;
353 exe = serverOn system dummyVersion;
354 server = args@{ hostName, adminEmail, routeHost, enableHttps, version }:
355 server (args // { exe = linuxExe version; });
356 obelisk = import (base + "/.obelisk/impl") {};
357 };
358 haskellPackageSets = {
359 inherit (reflex-platform) ghc ghcjs;
360 };