avoid trap-clobbering in nix-shell

If you've used nix-shell to test anything with a server component, you've likely added a trap to shellHook in order kill the server whenever you exit the shell.

I, for one, set up the trap and moved on with my life without giving it too much thought--but Nix and and the nixpkgs stdenv already have plans for the exit trap, and adding our own trap clobbers them. What gets clobbered? I wouldn't call any of it essential--but there are probably a few people out there wondering why tmpdirs are accumulating or showBuildStats doesn't work.

If you're skeptical, let's look at a simple shell.nix file:

simple-shell.nix
 1{ pkgs ? import <nixpkgs> {} }:
 2
 3pkgs.mkShell {
 4  buildInputs = [];
 5
 6  # make it obvious when nix-shell's hook runs
 7  showBuildStats = true;
 8
 9  shellHook = ''
10    exit
11  '';
12}

And the output we get from entering it:

nix-shell --pure simple-shell.nix
1build times:
2user time for the shell             0m0.000s
3system time for the shell           0m0.001s
4user time for all child processes   0m0.000s
5system time for all child processes 0m0.000s

Now let's try trapping EXIT from the shellHook:

infomercial-shell.nix
 1{ pkgs ? import <nixpkgs> {} }:
 2
 3pkgs.mkShell {
 4  buildInputs = [];
 5
 6  # make it obvious when nix-shell's hook runs
 7  showBuildStats = true;
 8
 9  shellHook = ''
10    trap 'echo Normally this would clobber the nix-shell EXIT trap' EXIT
11    exit
12  '';
13}

It clobbers the trap, leaving us with no timing information:

nix-shell --pure infomercial-shell.nix
1Normally this would clobber the nix-shell EXIT trap

To be fair, I didn't realize any of this was here until I ran into a slightly different version of the problem last year. I have a history module in my profile that also expects access to the exit trap, and for months I had a languishing TODO about looking into why some of the commands I ran inside a nix-shell weren't making it into my history. The inverse, of course, is also true: just like we can clobber the ~canonical exit traps from our little shellHook, Nix and the stdenv will happily obliterate an exit trap we set beforehand.

To fix this problem, I created a Shell library called comity. It's a bit like a get-along shirt for Shell modules/libraries that treat trap like they own it. Here's a repeat of the initial example + comity:

shell.nix using comity
 1{ pkgs ? import <nixpkgs> {} }:
 2
 3let
 4  comity = (builtins.getFlake "github:abathur/comity").packages.${builtins.currentSystem}.default;
 5in pkgs.mkShell {
 6  buildInputs = [ comity ];
 7
 8  # make it obvious when nix-shell's hook runs
 9  showBuildStats = true;
10
11  shellHook = ''
12    source comity.bash
13    trap 'echo Normally this would clobber the nix-shell EXIT trap' EXIT
14    exit
15  '';
16}
nix-shell --pure shell.nix
1build times:
2user time for the shell             0m0.001s
3system time for the shell           0m0.001s
4user time for all child processes   0m0.000s
5system time for all child processes 0m0.000s
6Normally this would clobber the nix-shell EXIT trap

comity is part of a broader vision that I call "Neighborly Shell". You can read a little more about this vision (and comity) in neighborly Shell with bashup.events.

Discussing this elsewhere?
Enter 'link' for a markdown link
or 'tweet <message>' for a pre-populated Tweet :)
Want to subscribe? Enter 'rss' for a feed URL.
>