Using KeePassHttp plugin on NixOS

I’m a long time user of KeePass 2 password manager, even so it is actually a Windows .NET application, that runs on Mono. It has two feature that are important to me, that the KeePass for Linux lacks:

  • synchronize the underlying file on save (I share the .kdbx file using ownCloud), instead of just overwriting it (eliminating changes to the database from other hosts)
  • reference credentials from other entries

… and as I’m back on NixOS, I of course wanted to have KeePass with the KeePassHttp for Chromium integration. On Ubuntu installation is trivial, KeePass itself is packaged, you take the plugin’s .plgx file and put it (as root) next to the KeePass.exe file somewhere under /usr/share.

Oh well, /nix/store is read-only, hence different approach needed.

Turns out that keepass2 NixOS package already has a plugin field, which just is set to an empty list. Therefore once you got the syntax it’s pretty straight forward…

That’s what my /etc/nixos/configuration.nix file looks like (relevant parts):

{ config, pkgs, ... }:

let

  keepassWithPlugins = pkgs.keepass.override {
    plugins = [
      pkgs.keepass-keepasshttp
    ];
  };

in

{
  # ... primary part of configuration.nix goes here ...

  # List packages installed in system profile. To search by name, run:
  # $ nix-env -qaP | grep wget
  environment.systemPackages = with pkgs; [

    # ... other packages ...
    keepassWithPlugins
  ];
}

Huginn loves Todoist, pt. 2

As written about here before I’m heavily relying on Todoist to stay organized, and one part of the story (so far) have been three recurring tasks on my todo list:

Screenshot of those three tood items

… they are just there to nag me every morning to consider what’s in my Inbox (sitting there waiting to be assigned to projects and otherwise classified), what’s over-due (to re-schedule or postpone) or labelled @waiting_for (I assign that label to all tasks that wait for someone else; and I skim over them reconsidering whether they got actionable meanwhile or need me poking). It’s just a routine, I briefly go over them and tick them off.

Yet sometimes the Inbox is empty or there just are no items labelled @waiting_for. It’s not much of a deal but I felt like having to automate that – i.e. just create those tasks if there are items in the inbox, overdue or waiting for …

Well, as I already have Huginn connected to my Todoist it was pretty clear that Huginn should also do that. So I had a look at the ruby-todoist-api Gem I’m already using for the TodoistAgent I wrote a month ago … turns out it has a query API, yet it isn’t as flexible as the filter expressions supported by Todoist’s web frontend. It indeed allows you to do simple queries like today or tomorrow or over due. But it doesn’t allow to search for projects, neither does it allow to combine queries with boolean operators (like (today | overdue) & #work).

Yet another topic that escalated quickly, … I kicked off a new project Todoist Querynaut, a Ruby gem, that has a Treetop-based parser for Todoist’s query language, uses the somewhat limitted API mentioned above and does the rest on the client-side. So if you query (today | overdue) it actually does two calls to the REST API and combines the items returned from both queries (filtering out duplicates). So far Querynaut is still in its’ early days, yet already usable. It doesn’t yet support some fancy kinds of queries (filtering by due date, to mention one), but the outline is there.

The next part then was to extend Huginn by another agent, which I named TodoistQueryAgent. It takes a query, executes it (via querynaut) and either emits an event for each and every task found (for mode=item) or just emits the number of search results (for mode=count).

For the use-case from above I created three new agents scheduled for 8am every day, went on setting mode to count and using p:Inbox, over due and @waiting_for as query strings. Then I connected those three agents to three more Trigger Agents that just compare the number of found tasks to be larger than zero and emit a suitable message that ends up on Todoist – those of course are connected to a Todoist Agent that properly forwards then. Like so:

Screenshot of Huginn Agent visualisation

In case you’d like to import this scenario, here’s my Huginn’s scenario JSON export. For that to work you need to have TodoistAgent 0.5.0 or newer installed.

Automating my Todoist with Huginn

So for some time now I’m using Todoist as my personal means of organization and (at least try to) practise GTD. It really helps me to stay organised (mostly) and keep focus, and even so non-free I’m using Todoist as it has both a nice and user-friendly Web UI as well as an Android App (both working well offline).

And then there’s Huginn which is a self-hosted IFTTT on steroids, which I’m also using for quite a while now. Yet so far I didn’t connect both tools.

Then there was that idea:

I tend to just “like” stuff over on Twitter to flag it for me to eventually “Read Somewhen”. Yet I use Todoist to keep a GTD-style somewhen maybe list.

So wouldn’t it be cool if tweets I liked would automatically pop up on my Todoist Inbox (at least if they are likely “stuff to read”)?

well, so I quickly noticed that Huginn doesn’t have a Todoist Agent and I’m not at all proficient in Ruby … anyways I gave it a try … so now there is huginn_todoist_agent :-)

In order to “click together” the Twitter-to-Todoist forwarder I created a scenario using three agents:

  1. Twitter Favorites Agent to continuously retrieve my Twitter favs and create an event for each and everyone
  2. a Trigger Agent consuming these events and filtering out stuff that’s not likely “to be read”
  3. last but not least the my own Todoist Agent configured with a “Huginn” label so I know where the tasks come from

The Trigger Agent is configured like this

{
  "expected_receive_period_in_days": "5",
  "keep_event": "false",
  "rules": [
    {
      "type": "regex",
      "value": "^http",
      "path": "entities.urls[0].url"
    },
    {
      "type": "!regex",
      "value": "^https://twitter.com/attn/status/",
      "path": "entities.urls[0].expanded_url"
    }
  ],
  "message": "Potential someday read: "
}

… it simply excludes all Tweets that either have no URL at all (so nothing to read there) or Tweets just mentioning other Tweets.

Using my “custom” Todoist Agent with Huginn’s docker container is pretty simple: you just provide an environment variable ADDITIONAL_GEMS with value huginn_todoist_agent and it auto-installs it during first start of the container :-)

solving social problems with technology

So I’m living together with my girlfriend for quite a while now, but there’s that one thing why we regularly get mad at each other: I’m sitting at my desk in the living room coding and she keeps talking to me every now and then, interupting my thoughts over and over … and me slowly getting more and more annoyed … and then she complains why I once more didn’t tell here that I’m trying to concentrate and if I just had told her …

As I’m practicing pomodoro technique (primarily to regularly take breaks and not sitting at my desk for hours straight) I already have the information. Starting to work I hit S-p hotkey and the pomodoro clock ticks down from 25 minutes to zero.

Wouldn’t it be great if only that information was available to my girlfriend? So I took one meter of WS2812 LED strip at 30 LEDs per meter, attached it to a bar made of cardboard and soldered a D1 mini to it …

problem solved, here’s what it looks like:

photo of the lightbar in pomodoro mode

At the beginning it shows 25 red LEDs followed by five green ones; clearly showing that I would not want to be interrupted. As the minutes go by the red LEDs turn off, one every minute. And if only green’s left, then there’s that perfect moment to start chatting :)

The software part of the D1 mini is pretty simple, it just connects to my local MQTT broker and fetches time via NTP; … and the shell script which is triggered by the aforementioned keyboard shortcut now just also publishes the start timestamp of current pomodoro to the lightbar control topic.

… and being at it I kept improving the software, adding various modes of ambient and attraction light modes :)

PS: and as I’m now already publishing the pomodoro information to MQTT the next step is to automatically switch my cell phone & tablet into DND mode during the work-phase of every pomodoro. Unfortunately turns out that that isn’t as easy going as expected as there’s no Tasker plugin that’s able to subscribe to a MQTT topic. Just some discussion on Reddit.

comparing the incomparible - #devcamp16 vs. #swec16

Lucky me, living in the metropolitan area of Nuremberg, … this year I attended three different developer-oriented open spaces hosted within one hour driving range from home.

  • devops camp in Nuremberg, six iterations so far, first one back in March 2014

And starting this year there are now two developer-centric events (both more or less claiming to be the first one):

  • developercamp in Würzburg, back in September 2016
  • swecamp in Tennenlohe/Erlangen, right yesterday & today

At the closing event of swecamp one participant mentioned that he was from Würzburg and also attended the developercamp… and that swecamp was really cool yet not on par with developercamp. Unfortunately he wasn’t willing to elaborate on what caused his feeling, but it kindof struck me … I couldn’t really answer the question which of those two camps was better (and remained quiet during the retrospective)…

But it kept bothering me and I had some time to think on may way home, … however I still cannot tell which one was “better”, mainly since it doesn’t feel right to compare the incomparible. After all both events attracted (slightly) different kinds of people. And as barcamps are largely shaped by their participators it’s due to this fact that both events felt different…

developercamp was organized by Mayflower which is a project & consulting agency in PHP & JavaScript field, also doing some Agile consulting, etc. … they mainly attracted (and this is personal gut feeling, not statistics) Webbish people, mostly developers but also a lot of product owners. Most attendees had an e-commerce background, be it a webshop, some shop software itself (Benjamin from Shopware was there) or some middleware provider. Topics were manifold, from What’s the job of a software architect? (which was interesting with half the audience from an Agile background and the other half from a more waterfallish one) over various Product Owner and discovery focussed stuff to dev-centric like Christopher’s TypeScript Game Engine Primer. Yet most sessions felt like fitting into my personal PHP/JavaScript/CleanCode filter bubble.

Opposed to that swecamp was organized by methodpark which also does projects and consulting, yet in more enterprisy contexts: medical devices and automotive. They managed to attract people from a (perceived) broader range of backgrounds … of course there were many of their own employees (Java, Embedded), more Java and C#, C++ and embedded devs from companies nearby … and of course also some Webbies (like me). The session topics were a bit more focussed on the engineering part and theoretical (read: language agnostic and less hands-on-code oriented) and a bit more testing. There were no Product Owner things and interestingly no operations stuff (Puppet, Ansible et al) and IoT was more of a topic (unfortunately I completely missed the IoT hack space)

What was especially cool with swecamp was that they had Susume Sasabe from Japan as a special guest, who did a comparison of DEV culture in Japan and Germany. He also spoke about Kaizen, different approaches to knowledge transfer, different problem solving methods etc. … all in all enriching the whole event every here and there. Besides that I really enjoyed @NullPlusEins’s sessions on (developer) psychology.

I also happily noticed that CQRS and Event Sourcing were a topic on both events, maybe a little bit more focussed on DDD at swecamp. This isn’t too surprising because of all the microservices buzz (which also was a hot topic on both events). Again serverless was not a topic, meh.

Last but not least swecamp was hosted in methodpark’s office which was way cosier and more comfortable, more decorated than the university building in Würzburg (where developercamp took place).

To sum it up, both events have their unique points. I really enjoyed developercamp (as a Web developer) and also swecamp, both had some sessions that really resonated with me (and after all that’s the main reason for me to go to unconferences: learning about things I don’t know that I don’t know them). I’m so happy that I don’t have to pick a single event to go to next year. Both organizers already told that they’ll follow up with a second iteration (and I’m more then willing to go to both of them)

PS: as a funny note, the devops camp site is the only one hosted with plain, unencrypted HTTP. Both more developer focussed events feature SSL :-)

PPS: developercamp also hosts on IPv6 and their hosting also supports Forward Secrecy, so let’s consider it better. SCNR :-)

Customizing my Keyboard Layout on NixOS

Here’s one missing write-up on a problem I immediately faced after reinstalling my Laptop with NixOS: my customized keyboard layout was missing. I’m using Programmer Dvorak for a prettly long time now. But I usually apply two modifications:

  • rearrange the numbers 0..9 from left to right (as on a “normal” keyboard, just using shift as modifier key)
  • add German umlauts to A, O and U keys (on modifier level 3)

… and use AltGr as modifier key to access level 3.

On Ubuntu I had a Puppet module around that used some augeas rules to patch /usr/share/X11/xkb/symbols/us as well as evdev.xml along it. There are several problems with this on NixOS after all: I don’t have puppet modifying system state, but the system is immutable. And after all there’s simply no such file :)

So pretty obviously modifying the keyboard layout is more involved on NixOS. I pretty quickly came to the conclusion that I would have to patch xkeyboard-config sources and use some form of package override.

Yet I had to tackle some learning curve first …

  • you cannot directly override xorg.xkeyboardconfig = ... as that would “remove” all the other stuff from below xorg. The trick is to override xorg with itself and merge in a (possibly recursive) hash with a changed version of xkeyboardconfig.
  • overriding xorg.xkeyboardconfig completely also turned out to be a bad idea as its indirectly included in almost every X.org derivation (so nixos-rebuild wanted to recompile LibreOffice et al – which I clearly didn’t want it to do)
  • almost close to frustration I found this configuration.nix Gist where someone obviously tries to do just the same – but the Gist doesn’t have many searchable terms (actually it’s just code), so it was really hard to find :) … his trick is to use overrideDerivation based on xorg.xkeyboardconfig but store it in a different variable. Then derive xorgserver, setxkbmap and xkbcomp and just use the modified xkeyboard configuration there

So here’s my change to /etc/nixos/configuration.nix:

  nixpkgs.config.packageOverrides = super: {
    xorg = super.xorg // rec {
      xkeyboard_config_dvp = super.pkgs.lib.overrideDerivation super.xorg.xkeyboardconfig (old: {
        patches = [
          (builtins.toFile "stesie-dvp.patch" ''
                      Index: xkeyboard-config-2.17/symbols/us
                      ===================================================================
                      --- xkeyboard-config-2.17.orig/symbols/us
                      +++ xkeyboard-config-2.17/symbols/us
                      @@ -1557,6 +1557,34 @@ xkb_symbols "crd" {
                         include "compose(rctrl)"
                       };
                       
                      +partial alphanumeric_keys
                      +xkb_symbols "stesie" {
                      +
                      +    include "us(dvp)"
                      +    name[Group1] = "English (Modified Programmer Dvorak)";
                      +
                      +    //             Unmodified       Shift           AltGr            Shift+AltGr
                      +    // symbols row, left side
                      +    key <AE01> { [ ampersand,       1                                           ] };
                      +    key <AE02> { [ bracketleft,     2,              currency                    ], type[Group1] = "FOUR_LEVEL_ALPHABETIC" };
                      +    key <AE03> { [ braceleft,       3,              cent                        ], type[Group1] = "FOUR_LEVEL_ALPHABETIC" };
                      +    key <AE04> { [ braceright,      4,              yen                         ], type[Group1] = "FOUR_LEVEL_ALPHABETIC" };
                      +    key <AE05> { [ parenleft,       5,              EuroSign                    ], type[Group1] = "FOUR_LEVEL_ALPHABETIC" };
                      +    key <AE06> { [ equal,           6,              sterling                    ], type[Group1] = "FOUR_LEVEL_ALPHABETIC" };
                      +
                      +    // symbols row, right side
                      +    key <AE07> { [ asterisk,        7                                           ], type[Group1] = "FOUR_LEVEL_ALPHABETIC" };
                      +    key <AE08> { [ parenright,      8,              onehalf                     ], type[Group1] = "FOUR_LEVEL_ALPHABETIC" };
                      +    key <AE09> { [ plus,            9                                           ], type[Group1] = "FOUR_LEVEL_ALPHABETIC" };
                      +    key <AE10> { [ bracketright,    0                                           ], type[Group1] = "FOUR_LEVEL_ALPHABETIC" };
                      +    key <AE11> { [ exclam,          percent,        exclamdown                  ], type[Group1] = "FOUR_LEVEL_ALPHABETIC" };
                      +    key <AE12> { [ numbersign,      grave,          dead_grave                  ] };
                      +
                      +    // home row, left side
                      +    key <AC01> { [ a,               A,              adiaeresis,      Adiaeresis ] };
                      +    key <AC02> { [ o,               O,              odiaeresis,      Odiaeresis ] };
                      +    key <AC04> { [ u,               U,              udiaeresis,      Udiaeresis ] };
                      +};
                       
                       partial alphanumeric_keys
                              xkb_symbols "sun_type6" {
                      Index: xkeyboard-config-2.17/rules/evdev.xml.in
                      ===================================================================
                      --- xkeyboard-config-2.17.orig/rules/evdev.xml.in
                      +++ xkeyboard-config-2.17/rules/evdev.xml.in
                      @@ -1401,6 +1401,12 @@
                               </variant>
                               <variant>
                                 <configItem>
                      +            <name>stesie</name>
                      +            <description>English (Modified Programmer Dvorak)</description>
                      +          </configItem>
                      +        </variant>
                      +        <variant>
                      +          <configItem>
                                   <name>rus</name>
                                   <!-- Keyboard indicator for Russian layouts -->
                                   <_shortDescription>ru</_shortDescription>
                           '')   # }
        ];
      });

      xorgserver = super.pkgs.lib.overrideDerivation super.xorg.xorgserver (old: {
        postInstall = ''
          rm -fr $out/share/X11/xkb/compiled
          ln -s /var/tmp $out/share/X11/xkb/compiled
          wrapProgram $out/bin/Xephyr \
            --set XKB_BINDIR "${xkbcomp}/bin" \
            --add-flags "-xkbdir ${xkeyboard_config_dvp}/share/X11/xkb"
          wrapProgram $out/bin/Xvfb \
            --set XKB_BINDIR "${xkbcomp}/bin" \
            --set XORG_DRI_DRIVER_PATH ${super.mesa}/lib/dri \
            --add-flags "-xkbdir ${xkeyboard_config_dvp}/share/X11/xkb"
          ( # assert() keeps runtime reference xorgserver-dev in xf86-video-intel and others
            cd "$dev"
            for f in include/xorg/*.h; do # */
              sed "1i#line 1 \"${old.name}/$f\"" -i "$f"
            done
          )
        '';
      }); 
  
      setxkbmap = super.pkgs.lib.overrideDerivation super.xorg.setxkbmap (old: {
        postInstall =
          ''
          mkdir -p $out/share
          ln -sfn ${xkeyboard_config_dvp}/etc/X11 $out/share/X11
          '';
      });
  
      xkbcomp = super.pkgs.lib.overrideDerivation super.xorg.xkbcomp (old: {
        configureFlags = "--with-xkb-config-root=${xkeyboard_config_dvp}/share/X11/xkb";
      });
    };
  };

After a nixos-rebuild switch I was able to setxkbmap us stesie to have my modified layout loaded. Last but not least I switched the default keyborad layout in configuration.nix like so:

  services.xserver.layout = "us";
  services.xserver.xkbVariant = "stesie";
  services.xserver.xkbOptions = "lv3:ralt_switch";

NixOS, PHP, PHPUnit & PhpStorm

Since one of my goals with NixOS was to conveniently install multiple PHP versions side by side (and without the hassle to compile manually with --prefix=/opt/php-something/ flags) I’ve finally tried to get a feasible setup.

For the moment I decided to go with “central” (i.e. not-per-project) installations within my $HOME as I don’t want to set up a whole environment just to do a Kata every once in a while.

Installing PHP 7.0

This should actually be pretty straight forward since PHP 7.0 as well as xdebug are both part of official nixpkgs … and once you know what to do it actually is :)

Obviously we need to create our own derivation and then can simply use buildEnv to symlink our buildInputs to the output directory. However the PHP binary has the extension path baked in (the one to its own nix store), so we have to overwrite that one. This can be done using makeWrapper script and simply provide -d extension_dir=... flag before anything else. In order to be able to do that we first need to fiddle with the /bin directory though, as only the php70 build input has a /bin directory and buildEnv hence simply links the whole $php70/bin directory to $out/bin.

I decided to put the PHP 7.0 environment to $HOME/bin/php70, which I mkdir‘ed first and then created a default.nix like this:

with import <nixpkgs> { };

stdenv.mkDerivation rec {
  name = "php70-env";

  env = buildEnv { 
    inherit buildInputs name;
    paths = buildInputs;
    postBuild = ''
      mkdir $out/bin.writable && cp --symbolic-link `readlink $out/bin`/* $out/bin.writable/ && rm $out/bin && mv $out/bin.writable $out/bin
      wrapProgram $out/bin/php --add-flags "-d extension_dir=$out/lib/php/extensions -d zend_extension=xdebug.so"
    '';
  };
  builder = builtins.toFile "builder.sh" ''
    source $stdenv/setup; ln -s $env $out
  '';

  buildInputs = [
    php70
    php70Packages.xdebug
    makeWrapper
  ];
}

(maybe there’s a more elegant solution to replace this link with a directory of symlinks, … but I haven’t found out yet. Please tell me in case you know how to do it properly)

… then after running nix-build you should have a ./result/bin/php which is a shell script wrapping the actual php binary to automatically load xdebug.so.

Installing PHP 7.1 (beta 3)

This is of course a little bit more involved as PHP 7.1 isn’t yet part of nixpkgs, since it’s not really released yet.

The installation (including compilation) of PHP 7.1 can be achieved by simply creating an overrideDerivation of pkgs.php70 with name, version and src adapted as needed.

… the xdebug PECL package from nixpkgs cannot simply be re-used as the latest release doesn’t support PHP 7.1 as of yet. But creating a derivation for an PECL package from scratch isn’t complicated, so let’s just do that manually :)

So here’s the php71/default.nix file:

with import <nixpkgs> { };

let
  php71 = pkgs.lib.overrideDerivation pkgs.php70 (old: rec { 
    version = "7.1.0beta3";
    name = "php-${version}";
    src = fetchurl {
      url = "https://downloads.php.net/~davey/php-${version}.tar.bz2";
      sha256 = "02gv98xaal8pdr1yj57k2ns4v8g53iixrz4dynb5nlr81vfg4gwi";
    };
  });

  php71_xdebug = stdenv.mkDerivation rec {
    name = "php-xdebug-55fccbb";
    src = fetchgit {
      url = "https://github.com/xdebug/xdebug";
      rev = "55fccbbcb8da0195bb9a7c332ea5364f58b9316b";
      sha256 = "15wgvzf7l050x94q0a62ifxi5j7p9wn2f603qzxwcxb7ximd9ffb";
    };
    buildInputs = [ php71 autoreconfHook ];

    makeFlags = [ "EXTENSION_DIR=$(out)/lib/php/extensions" ];
    autoreconfPhase = "phpize";
    preConfigure = "touch unix.h";
  };

in
  stdenv.mkDerivation rec {
    name = "php71-env";

    env = buildEnv { 
      inherit buildInputs name;
      paths = buildInputs;
      postBuild = ''
        mkdir $out/bin.writable && cp --symbolic-link `readlink $out/bin`/* $out/bin.writable/ && rm $out/bin && mv $out/bin.writable $out/bin
        wrapProgram $out/bin/php --add-flags "-d extension_dir=$out/lib/php/extensions -d zend_extension=xdebug.so"
      '';
    };
    builder = builtins.toFile "builder.sh" ''
      source $stdenv/setup; ln -s $env $out
    '';

    buildInputs = [
      php71
      php71_xdebug
      makeWrapper
    ];
  }

The touch unix.h thing is actually a hack needed to compile the extension properly as php’s config.h has the HAVE_UNIX_H flag defined, which should actually tell whether the system has a unix.h file (which old Unix systems obviously once had, Linux systems don’t). However the imap client library has a unix.h file which tricks php’s configure script to note having one :) … and as we (obviously) don’t have imap library as build input php.h’s #include would fail without that touch

PHPUnit & PhpStorm

PHPUnit isn’t packaged in nixpkgs, but after all it’s just a phar archive file and we’d like to run it with different PHP versions anyways, therefore I’ve just downloaded it and stored it in $HOME/bin also.

Configuring PhpStorm is pretty straight forward, just go to File > Default Settings > Languages & Frameworks > PHP and click the three-dots-button next to Interpreter. Then simply add both PHP executables at $HOME/bin/php70/result/bin/php and $HOME/bin/php71/result/bin/php. PhpStorm should automatically find out about the php.ini file as well as the Xdebug extension.

… last but not least go to PHPUnit config folder, choose Path to phpunit.phar and point it to the downloaded phar archive.

… and now you’re set :) Just select one of the interpreters and run your tests (and then switch interpreters to your liking).

Building a Jekyll Environment with NixOS

So there is this idea with NixOS to install only the very base system in the global environment and augment these using Development Environments. And as I’m creating this blog using Github Pages aka Jekyll, writing in Markdown, and would like to be able to preview any changes locally, I of course need Jekyll running locally. Jekyll is even on nixpkgs, … but there are Jekyll plugins which aren’t bundled with this package and essential for correct rendering of e.g. @-mentions and source code blocks.

… so the obvious step was to create such a NixOS Development Environment, which has Ruby 2.2, Jekyll and all the required plugins installed. Turns out there even is a github-pages Gem, so we just need to “package” that. Packaging Ruby gems is pretty straight forward actually, …

so first let’s create a minimal Gemfile first:

source 'https://rubygems.org'
gem 'github-pages'

The Github page has this , group: :jekyll_plugins thing in it, … I had to remove it, otherwise nix-shell complains that it cannot find the Jekyll gem file, once you try to run it (later).

Then we need to create Gemfile.lock by running bundler (from within a nix-shell that has bundler):

$ nix-shell -p bundler
$ bundler package --no-install --path vendor
$ rm -rf .bundler vendor
$ exit  # leave nix-shell

… and derive a Nix expression from Gemfile.lock like so (be sure to not accidentally run this command from within the other nix-shell, which would fail with strange SSL errors otherwise):

$ $(nix-build '<nixpkgs>' -A bundix)/bin/bundix
$ rm result   # nix-build created this (linking to bundix build)

… and last but not least we need a default.nix file which actually triggers the environment creation and also automatically starts jekyll serve after build:

with import <nixpkgs> { };

let jekyll_env = bundlerEnv rec {
    name = "jekyll_env";
    ruby = ruby_2_2;
    gemfile = ./Gemfile;
    lockfile = ./Gemfile.lock;
    gemset = ./gemset.nix;
  };
in
  stdenv.mkDerivation rec {
    name = "jekyll_env";
    buildInputs = [ jekyll_env ];

    shellHook = ''
      exec ${jekyll_env}/bin/jekyll serve --watch
    '';
  }

Take note of the exec in shellHook which actually replaces the shell which nix-shell is about to start by Jekyll itself, so once you stop it by pressing C-c the environment is immediately closed as well.

So we’re now ready to just start it all:

[stesie@faulobst:~/Projekte/stesie.github.io]$ nix-shell 
Configuration file: /home/stesie/Projekte/stesie.github.io/_config.yml
            Source: /home/stesie/Projekte/stesie.github.io
       Destination: /home/stesie/Projekte/stesie.github.io/_site
 Incremental build: enabled
      Generating... 
                    done in 0.147 seconds.
 Auto-regeneration: enabled for '/home/stesie/Projekte/stesie.github.io'
Configuration file: /home/stesie/Projekte/stesie.github.io/_config.yml
    Server address: http://127.0.0.1:4000/
  Server running... press ctrl-c to stop.