Servers with Haskell and NixOS

Chris Martin

Monadic Party 2018 June 14-15

Course goals

Prerequisites

Course outline

  1. What are Nix and NixOS
  2. Launching and configuring NixOS on EC2
  3. Deploying by rebuilding NixOS locally
  4. Building a Haskell project with Nix
  5. Nixpkgs version pinning
  6. Defining systemd units in NixOS
  7. Basic web server with Scotty
  8. Reverse proxying with nginx
  9. Systemd socket activation
  10. Journald and simple logging

What we are not going to do

My platform history

Nix

Origin

The Purely Functional Software Deployment Model, Eelco Dolstra, 2006

Portability is tricky

The main idea

The Purely Functional Software Deployment Model, Eelco Dolstra, 2006

GNU make vs Nix

Make Nix
Build unit Targets Derivations
Build file List of targets Lazy λ-calculus
Basis for rebuilds Timestamps of build inputs Hashes of build inputs
Where builds results go Where you specify In the Nix store (/nix/store/...)
How to remove old files A target called clean nix-collect-garbage
Composing build files include import
External dependencies ? nixpkgs

Example

https://archives.haskell.org/projects.haskell.org/diagrams/gallery/Hilbert.html

Makefile

all: hilbert-curve.png

sourceUrl = https://archives.haskell.org/projects.haskell.org/diagrams/gallery/Hilbert.lhs

hilbert-curve.lhs:
    wget $(sourceUrl) --output-document $@

hilbert-curve.svg: hilbert-curve.lhs
    runhaskell --ghc-arg=-XTypeFamilies $< --output $@ --width 250

hilbert-curve.png: hilbert-curve.svg
    convert $< $@

clean:
    rm -f *.lhs *.svg *.png

.PHONY: clean

default.nix

let
    pkgs = import <nixpkgs> {};

    src = pkgs.fetchurl {
        url = https://archives.haskell.org/projects.haskell.org/diagrams/gallery/Hilbert.lhs;
        sha256 = "0bplymiysf4k9vlw2kf5dfzgk1w2r0w5pbr8m24wjk045rml5hqq";
    };

    ghc = pkgs.haskellPackages.ghcWithPackages (p: [ p.diagrams ]);

    svg = pkgs.runCommand "hilbert-curve.svg"
        { buildInputs = [ ghc ]; }
        "runhaskell --ghc-arg=-XTypeFamilies ${src} -o $out -w 250";

    png = pkgs.runCommand "hilbert-curve.png"
        { buildInputs = [ pkgs.imagemagick ]; }
        "convert ${svg} $out";

in
    png

Annoyances with the Makefile

Why Nix for servers?

NixOS config file

AWS

Launching on EC2

https://nixos.org/nixos/download.html

Step 2 – Instance type

We’re going very minimal here

WHAT 1GB of mem is good enough? 🤔 skepticalface is skeptical.

Step 3 – More configuration

Default configuration.nix for EC2 instances

{
    imports = [ <nixpkgs/nixos/modules/virtualisation/amazon-image.nix> ];

    ec2.hvm = true;
}

https://nixos.org/nixos/options.html

bootstrap.nix

{
    imports = [ <nixpkgs/nixos/modules/virtualisation/amazon-image.nix> ];

    config = {
        ec2.hvm = true;

        # Run SSH on a weird port
        services.openssh.ports = [ 46861 ];
        networking.firewall.allowedTCPPorts = [ 46861 ];

        # Don't allow anyone to SSH using a password
        services.openssh.passwordAuthentication = false;

        # Let admins sudo without entering a password
        security.sudo.wheelNeedsPassword = false;

        # Let admins upload unsigned Nix packages
        nix.trustedUsers = [ "@wheel" ];

        users.users.chris = {
            isNormalUser = true;
            extraGroups = [ "wheel" ];

            openssh.authorizedKeys.keys = [
                "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDFJ4UYsEh+JZQGCMdbNrKfjH1F3rwKBRewwgaehwnijBADYSJ8iwDZji09vVfCxSQSMjZJS54mEBtjcOBOpM7+mR585wI6jhsfdsNqNwzJdxV47Bi4jAkg7XlWf9IYv7EUhRzsKGdoSqefh/7bN6MPcJQ9ccHKqBxtmGJ6eHfgLmgnb8+ozwDlwQKz5QDtdEnt8TUqucUB4AOyReBV7GRnwkTyGCForb5nhTftuVi7GO1qApJKBIpYlC1gbuCWDX9CIl7IzfAMyng6u6Ty9x31ZWKA0sJzRIX5cw3e8Ct7sWzZB3O/2FOwjyYadqTRQdR472Dz/f6mqqIl1ioxzfXRfh33bREg2opLc6bnYaWTXY6aAc5/wUbC7z4CTKBGZJHxY5mrRSlpQ2Rn8EvgyyxgxokLdTZqoiKw/tSmE9Mlle5JGh+m8agGe41dszZxBf41j/ORE+N5p0k02fvUWuG0PL3aFE77qUbOgxxXOYMtBV0YiJPzeBXDGrkW1wqKC2voJ6PuCZWOHaLxDqkUDgAMYyGMKoj5C53OZneVeSMgZG+/lxygAduyBx/RfQYrt4WsPfPnhl95Kxx8PTYuFfLXmcMNMhZ7rYW+Thvo40W+VjiqTUSCxLHr16SFSOj2mGl0A29VPPHA3H+ckprCo8pldPo3AYrwkV/zHlyLjuEQfQ== renzo-1"
            ];
        };

        users.users.julie = {
            isNormalUser = true;
            extraGroups = [ "wheel" ];

            openssh.authorizedKeys.keys = [
                "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDUA5QiRaMriKQrvA1H74UsJcnGieAcHYrHp27KBZjOhhKvinTkNyErY5JNFbgrCh3oC+6HeMSUOp+5qb2/hF0aO6kdYbGnYB3BchrbsPYFDccqW9eQONXdvYsa4mavfLzHIvXktkxVsd71CBRXqVFxxolYps99iJvFfhmgnn9Iz+zUIuOSRCPQ/Bcxbh3+fZHD2wlPpOZ352V3/utR6n6nlhK5W1Gq/mp5dmE32zCEOEse85xXQNxJpjdhIDZbu8PnNo2LuYIfRBFzIj/Gh6MIHJU3gQskt8MvKA+POdseflPLznxoDWYT3FqBO+agObr3FGnlnwpi1g9ym+U8T6SNFGUwR7cX5VkegLNSl7FTLMtaXoqUTKhBQH+ZIpNwlyepYnFo1BHR3IgsFDSaD/zLUjesBQIw6j+mtDCK9P3EnUcXo5v04OuGoyqTtAF4TTAz2kuC8ZsCs0cQEZRIoXqVIRcvPmlFr208o0SeGakCHVxIj6VnrQ+aHisVwuGAkq+Kb5mwE6b0xvuFGHo+bqHIUePymWQihbh0pZpYeSaJTLb2YG17HmE2rUFeO66CADmIMi2EWEI0xisT/e9FoqDcMnG/Z5sLBBDkerXwyfLA63zAkF0P3LtWX1Q0vYFhCn+VtMwE56qW/Z0sDdNiVwxn/a1GGh12gqXkNHBEePYjoQ== doriangray"
            ];
        };
    };
}

Step 4 – Storage

Step 5 – Tags

Step 6 – Security group

Step 7 – Review

Launching via command-line

❮bash❯ aws ec2 run-instances [... a bunch of args ...]

Deploy strategy

Deploy strategy

Deploy strategy

server.nix

let
    nixos = import <nixpkgs/nixos> {
        configuration = {
            imports = [
                ./modules/bootstrap.nix
            ];
            config = {
            };
        };
    };
in
    nixos.system

Building NixOS locally

❮bash❯ nix-build server.nix
[... a bunch of build output ...]
/nix/store/bbcbh0vv64v5bgdhdd4a8fw3p438iiam-nixos-system-unnamed-18.03.132618.0f73fef53a9

❮bash❯ ls -lh
ls -lh
total 12K
-rw-r--r-- 1 chris users 2.2K Jun  7 02:30 bootstrap.nix
lrwxrwxrwx 1 chris users   89 Jun  7 02:35 result -> /nix/store/pvf3l9x8m2fnw7l07g1yxw1iilj3m2rp-nixos-system-unnamed-18.03.132618.0f73fef53a9
-rw-r--r-- 1 chris users  176 Jun  7 02:30 server.nix

The build result

❮bash❯ ls -lh result/
total 60K
dr-xr-xr-x 2 root root 4.0K Dec 31  1969 bin
dr-xr-xr-x 2 root root 4.0K Dec 31  1969 fine-tune
-r-xr-xr-x 1 root root  13K Dec 31  1969 activate
lrwxrwxrwx 1 root root   91 Dec 31  1969 append-initrd-secrets -> /nix/store/m2jf41zpw52i10j01p4wij6j2h22721j-append-initrd-secrets/bin/append-initrd-secrets
-r--r--r-- 1 root root    0 Dec 31  1969 configuration-name
lrwxrwxrwx 1 root root   51 Dec 31  1969 etc -> /nix/store/iwpgvdg2i4vv6a0vd20d70425s0rifdh-etc/etc
-r--r--r-- 1 root root    0 Dec 31  1969 extra-dependencies
lrwxrwxrwx 1 root root   65 Dec 31  1969 firmware -> /nix/store/grzr4r1ajmnjjnq049r719jbj2p8zbcp-firmware/lib/firmware
-r-xr-xr-x 1 root root 4.9K Dec 31  1969 init
-r--r--r-- 1 root root    9 Dec 31  1969 init-interface-version
lrwxrwxrwx 1 root root   57 Dec 31  1969 initrd -> /nix/store/bnqw7sxz3pnakni5hmj22dqrqvjbbq1m-initrd/initrd
lrwxrwxrwx 1 root root   65 Dec 31  1969 kernel -> /nix/store/y0jcw44kw1962bdfnsayz1r6jsgz2sqj-linux-4.14.44/bzImage
lrwxrwxrwx 1 root root   58 Dec 31  1969 kernel-modules -> /nix/store/yw9zkbwkb76ypb0gvvakg0lkv1zirqg7-kernel-modules
-r--r--r-- 1 root root   51 Dec 31  1969 kernel-params
-r--r--r-- 1 root root   24 Dec 31  1969 nixos-version
lrwxrwxrwx 1 root root   55 Dec 31  1969 sw -> /nix/store/j85qcbjfwmis0c75qwbvxhirlj02kjmw-system-path
-r--r--r-- 1 root root   12 Dec 31  1969 system
lrwxrwxrwx 1 root root   55 Dec 31  1969 systemd -> /nix/store/cya0k4g78bd518wzgqghkk6srlvl2jgv-systemd-237

deploy.hs

Standard boilerplate

#! /usr/bin/env stack
-- stack script --resolver lts-11.8 --no-nix-pure

{-# LANGUAGE OverloadedStrings #-}

import Turtle

main :: IO ()
main = ...

main

main :: IO ()
main =
  sh $ do
    server <- getServer   -- Read the server address from a file
    path <- build         -- (1) Build NixOS for our server
    upload server path    -- (2) Upload the build to the server
    activate server path  -- (3) Start running the new version

Types

-- The path of the NixOS build that we're deploying, e.g.
-- "/nix/store/bbcbh0vv64v5bgdhdd4a8fw3p438iiam-nixos-system-unnamed-18.03.132618.0f73fef53a9"
newtype NixOS = NixOS Text

-- The address of the server to which we're deploying, e.g. "54.175.33.139"
newtype Server = Server Text

getServer

-- Read the server address from a file.
getServer :: Shell Server
getServer =
  do
    line <- single (input "server-address.txt")
    return (Server (lineToText line))

build

-- Build NixOS for our server.
build :: Shell NixOS
build =
  do
    line <- single (inproc command args empty)
    return (NixOS (lineToText line))

  where
    command = "nix-build"
    args = ["server.nix", "--no-out-link"]

upload

-- Upload the build to the server.
upload :: Server -> NixOS -> Shell ()
upload (Server server) (NixOS path) =

    procs command args empty

  where
    command = "nix-copy-closure"
    args = ["--use-substitutes", server, path]

activate

{-# LANGUAGE QuasiQuotes #-}
import NeatInterpolation (text)

-- Start running the new version of NixOS on the server.
activate :: Server -> NixOS -> Shell ()
activate (Server server) (NixOS path) =

    procs command args empty

  where
    command = "ssh"
    args = [server, remoteCommand]
    profile = "/nix/var/nix/profiles/system"
    remoteCommand = [text|
        nix-env --profile $profile --set $path
        $profile/bin/switch-to-configuration switch
      |]

Running the deploy script

❮bash❯ ./deploy.hs
[... a bunch of build output ...]
updating GRUB 2 menu...
activating the configuration...
setting up /etc...
setting up tmpfiles

Haskell project structure

Haskell project structure

monadic-party/ghc-version.nix

"ghc822"

monadic-party/default.nix

{ pkgs }:

let

    ghc = pkgs.haskell.packages.${import ./ghc-version.nix};

    developPackage =
      { root
      , source-overrides ? {}
      , overrides ? self: super: {}
      }:
      let
          extensions = pkgs.lib.composeExtensions
              (ghc.packageSourceOverrides source-overrides)
              overrides;

          ghc' = ghc.extend extensions;

          package = ghc'.callCabal2nix (builtins.baseNameOf root) root {};

      in
          pkgs.haskell.lib.overrideCabal package (drv: {
              enableSharedExecutables = false;
              isLibrary = false;
              doHaddock = false;
              postFixup = "rm -rf $out/lib $out/nix-support $out/share/doc";
          });

in

developPackage {

  root = ./.;

  source-overrides = {

    # Any specific Haskell package versions you need to pin go here, e.g.:
    # scotty = "0.11.1";

  };

}

monadic-party/shell.nix (basic)

let
    pkgs = import <nixpkgs> { config.allowUnfree = true; };

    monadic-party = import ./default.nix { inherit pkgs; };

in
    monadic-party

monadic-party/shell.nix (with dev tools)

let
    pkgs = import <nixpkgs> { config.allowUnfree = true; };

    monadic-party = import ./default.nix { inherit pkgs; };

    ghc = pkgs.haskell.packages.${import ./ghc-version.nix};

    cabal = ghc.cabal-install;

    party-ghci = pkgs.writeScriptBin "party-ghci"
        ''
            ${cabal}/bin/cabal new-repl "$@"
        '';

    party-ghcid = pkgs.writeScriptBin "party-ghcid"
        ''
            ${ghc.ghcid}/bin/ghcid --command "${cabal}/bin/cabal new-repl" "$@"
        '';

    devTools = [
        party-ghci
        party-ghcid
    ];

in
    monadic-party.env.overrideAttrs (attrs: {
        buildInputs = attrs.buildInputs ++ devTools;
    })

Using our dev tools

❮bash❯ nix-shell --pure --run 'party-ghci'
In order, the following will be built (use -v for more details):
 - monadic-party-0 (lib) (first run)
Preprocessing library for monadic-party-0..
GHCi, version 8.2.2: http://www.haskell.org/ghc/  :? for help
Loaded GHCi configuration from /home/chris/.ghc/ghci.conf
[1 of 1] Compiling PartyServer      ( lib/PartyServer.hs, interpreted )
Ok, one module loaded.

λ>

monadic-party/monadic-party.cabal

name: monadic-party
version: 0

cabal-version: >= 1.10
build-type: Simple

library
    hs-source-dirs: lib

    exposed-modules:
        MonadicParty.Scotty

    build-depends:
        base
      , neat-interpolation
      , scotty
      , text

    ghc-options: -Wall
    default-language: Haskell2010

executable party-scotty
    hs-source-dirs: app
    main-is: party-scotty.hs

    build-depends:
        base
      , monadic-party

    ghc-options: -Wall
    default-language: Haskell2010

Web server

Haskell libraries for web servers

monadic-party/lib/MonadicParty/Scotty.hs

{-# LANGUAGE OverloadedStrings, QuasiQuotes #-}

module MonadicParty.Scotty (main) where

-- neat-interpolation
import NeatInterpolation

-- scotty
import qualified Web.Scotty as Scotty

-- text
import qualified Data.Text.Lazy

main :: IO ()
main =
    Scotty.scotty 8000 scottyApp

scottyApp :: Scotty.ScottyM ()
scottyApp =
    Scotty.get "/scotty" $
        Scotty.html (Data.Text.Lazy.fromStrict [text|
            <!doctype html>
            <html>
                <head></head>
                <body>
                    Hello, I am a very basic Scotty demo,
                    serving (internally) on port 8000.
                </body>
            </html>
          |])

server.nix

let
    nixos = import <nixpkgs/nixos> {
        configuration = {
            imports = [
                ./modules/bootstrap.nix
                ./modules/party-scotty.nix
                ./modules/nginx.nix
            ];
            config = {
                networking.hostName = "monadic-party";
                nixpkgs.config.allowUnfree = true;
                users.users.monadic-party = {};
            };
        };
    };
in
    nixos.system

modules/party-scotty.nix

{ pkgs, ... }:
{
    imports = [
    ];
    config = {
        systemd.services.party-scotty = {
            enable = true;
            description = "Monadic Party - Scotty";
            wantedBy = ["multi-user.target"];

            environment = {
                LC_ALL = "en_US.UTF-8";
                LOCALE_ARCHIVE = "${pkgs.glibcLocales}/lib/locale/locale-archive";
            };

            serviceConfig = {
                User = "monadic-party";
                Restart = "on-failure";
                ExecStart = "${import ../monadic-party { inherit pkgs; }}/bin/party-scotty";
            };
        };
    };
}

modules/nginx.nix

{ pkgs, ... }:
{
    imports = [
    ];
    config = {

        networking.firewall.allowedTCPPorts = [ 80 443 ];

        services.nginx = {
            enable = true;

            recommendedGzipSettings = true;
            recommendedOptimisation = true;
            recommendedProxySettings = true;
            recommendedTlsSettings = true;

            virtualHosts."monadic-party.chris-martin.org" = {

                enableACME = true;
                addSSL = true;

                locations."/scotty".proxyPass =
                    "http://localhost:8000";
            };
        };
    };
}

Haskell on NixOS – Day 2

Review

Review (2)

Haskell project setup:

Review (3)

Nix tools we’ve seen:

Review (4)

NixOS modules on our server:

Systemd-activated sockets

The problem

502 Bad Gateway

Socket activation

modules/party-socket.nix

{ pkgs, ... }:
{
    config = {
        systemd.services.party-socket = {
            enable = true;
            description = "Monadic Party - Socket";
            wantedBy = ["multi-user.target"];
            requires = ["party-socket.socket"];

            environment = {
                LC_ALL = "en_US.UTF-8";
                LOCALE_ARCHIVE = "${pkgs.glibcLocales}/lib/locale/locale-archive";
            };

            serviceConfig = {
                User = "monadic-party";
                Restart = "on-failure";
                ExecStart = "${import ../monadic-party { inherit pkgs; }}/bin/party-socket";
            };
        };

        systemd.sockets.party-socket = {
            wantedBy = ["sockets.target"];
            socketConfig = {
                ListenStream = "/run/party-socket.socket";
            };
        };
    };
}

socket-activation Haskell library

module Network.Socket.Activation where
-- | Return a list of activated sockets, if the program was started
-- with socket activation. The sockets are in the same order as in
-- the associated .socket file.
getActivatedSockets :: IO (Maybe [Socket])

socket-activation Haskell library (2)

-- | Return a list of activated sockets, if the program was started
-- with socket activation. The sockets are in the same order as in
-- the associated .socket file.
getActivatedSockets :: IO (Maybe [Socket])
getActivatedSockets =
  runMaybeT $ do
    listenPid <- read <$> MaybeT (getEnv "LISTEN_PID")
    listenFDs <- read <$> MaybeT (getEnv "LISTEN_FDS")
    myPid     <- lift getProcessID
    guard $ listenPid == myPid
    mapM makeSocket [fdStart .. fdStart + listenFDs - 1]
  where
    fdStart = 3

Functions to start Scotty

-- | Run a scotty application using the warp server.
scotty :: Port -> ScottyM () -> IO ()

-- | Run a scotty application using the warp server, passing extra options.
scottyOpts :: Options -> ScottyM () -> IO ()

-- | Run a scotty application using the warp server, passing extra options,
-- and listening on the provided socket.
scottySocket :: Options -> Socket -> ScottyM () -> IO ()

Scotty Options

data-default-class Haskell library

module Data.Default.Class where

-- | A class for types with a default value.
class Default a where

    -- | The default value for this type.
    def :: a

TypeApplications

network Haskell library

module Network.Socket where

fdSocket :: Socket -> CInt

-- | Set the nonblocking flag on Unix.
setNonBlockIfNeeded :: CInt -> IO ()

monadic-party/lib/MonadicParty/Socket.hs

{-# LANGUAGE OverloadedStrings, QuasiQuotes,
             ScopedTypeVariables, TypeApplications #-}

module MonadicParty.Socket (main) where

-- base
import System.Exit (die)

-- data-default-class
import Data.Default.Class (def)

-- neat-interpolation
import NeatInterpolation

-- network
import Network.Socket (Socket)
import qualified Network.Socket as Socket

-- scotty
import qualified Web.Scotty as Scotty

-- socket-activation
import Network.Socket.Activation (getActivatedSockets)

-- text
import qualified Data.Text.Lazy

main :: IO ()
main = do

    socketsMaybe :: Maybe [Socket] <- getActivatedSockets

    case socketsMaybe of

        Just [socket] ->
          do
            Socket.setNonBlockIfNeeded (Socket.fdSocket socket)
            Scotty.scottySocket (def @Scotty.Options) socket scottyApp

        Just sockets -> die ("Wrong number of sockets: " ++
                             show (length sockets))

        Nothing -> die "Not socket activated"

scottyApp :: Scotty.ScottyM ()
scottyApp =
    Scotty.get "/socket" $
        Scotty.html (Data.Text.Lazy.fromStrict [text|
            <!doctype html>
            <html>
                <head></head>
                <body>
                    This one is socket activated.
                </body>
            </html>
          |])

Another possibility

main :: IO ()
main = do

    socketsMaybe :: Maybe [Socket] <- getActivatedSockets

    case socketsMaybe of

        Nothing ->
            Scotty.scotty 8001 scottyApp

        Just [socket] ->
          do
            Socket.setNonBlockIfNeeded (Socket.fdSocket socket)
            Scotty.scottySocket (def @Scotty.Options) socket scottyApp

        Just sockets -> die ("Wrong number of sockets: " ++
                             show (length sockets))

server.nix

let
    nixos = import <nixpkgs/nixos> {
        configuration = {
            imports = [
                ./modules/bootstrap.nix
                ./modules/party-scotty.nix
                ./modules/party-socket.nix
                ./modules/nginx.nix
            ];
            config = {
                networking.hostName = "monadic-party";
                nixpkgs.config.allowUnfree = true;
                users.users.monadic-party = {};
            };
        };
    };
in
    nixos.system

modules/nginx.nix

{ pkgs, ... }:
{
    imports = [
    ];
    config = {

        networking.firewall.allowedTCPPorts = [ 80 443 ];

        services.nginx = {
            enable = true;

            recommendedGzipSettings = true;
            recommendedOptimisation = true;
            recommendedProxySettings = true;
            recommendedTlsSettings = true;

            virtualHosts."monadic-party.chris-martin.org" = {

                enableACME = true;
                addSSL = true;

                locations."/scotty".proxyPass =
                    "http://localhost:8000";

                locations."/socket".proxyPass =
                    "http://unix:/run/party-socket.socket";

            };
        };
    };
}

Curling a Unix socket

❮bash❯ curl --unix-socket /run/party-socket.socket dummydomain/socket
hello

Nixpkgs version pinning

Why pinning

NIX_PATH

NIX_PATH (2)

nixpkgs.nix

# nix-prefetch-url --unpack https://github.com/NixOS/nixpkgs/archive/<rev>.tar.gz

(import <nixpkgs> { }).fetchFromGitHub {
  owner = "NixOS";
  repo = "nixpkgs";

  # NixOS 18.03, 2018 Mar 18
  rev = "0e7c9b32817e5cbe61212d47a6cf9bcd71789322";
  sha256 = "1dm777cmlhqcwlrq8zl9q2d87h3p70rclpvq36y43kp378f3pd0y";
}

deploy.hs

main :: IO ()
main =
  sh $ do
    nixpkgs <- getNixpkgs -- Get the pinned nixpkgs
    setNixPath nixpkgs    -- Set the NIX_PATH environment variable

    server <- getServer   -- Read the server address from a file
    path <- build         -- (1) Build NixOS for our server
    upload server path    -- (2) Upload the build to the server
    activate server path  -- (3) Start running the new version

Nixpkgs

newtype Nixpkgs = Nixpkgs Text

getNixpkgs

getNixpkgs :: Shell Nixpkgs
getNixpkgs =
  do
    line <- single (inproc command args empty)
    return (Nixpkgs (lineToText line))

  where
    command = "nix-build"
    args = ["nixpkgs.nix", "--no-out-link"]

setNixPath

setNixPath :: Nixpkgs -> Shell ()
setNixPath (Nixpkgs path) =
    export "NIX_PATH" ("nixpkgs=" <> path)

Logging

Logging approach

A tiny logging framework

A queue of Text

-- stm
import Control.Concurrent.STM (atomically)
import Control.Concurrent.STM.TChan
    (TChan, newTChanIO, readTChan, writeTChan)

data LogHandle = LogHandle (TChan Text)

newLog

newLog :: IO LogHandle
newLog =
    LogHandle <$> newTChanIO

writeToLog

writeToLog :: LogHandle -> Text -> IO ()
writeToLog (LogHandle chan) message =
    atomically (writeTChan chan message)

runLogger

import Control.Monad (forever)

runLogger :: LogHandle -> IO a
runLogger (LogHandle chan) =
    forever $ do
        message <- atomically (readTChan chan)
        Data.Text.IO.putStrLn message

async Haskell library

concurrently_ :: IO a -> IO b -> IO ()

Main with a logger

import Control.Concurrent.Async (concurrently_)
import qualified System.IO as IO

main :: IO ()
main = do
    IO.hSetBuffering IO.stdout IO.LineBuffering
    logHandle <- newLog
    concurrently_ (runLogger logHandle) runServer

runServer :: IO a
runServer = undefined