Making Cypress Work in NixOS

2021-03-02

Table of Contents

Introduction

I recently wanted to refactor a small Javascript code base at work. Especially when working in a dynamic language, I always make sure that the code in question is well tested, because refactoring without tests is a nightmare.

In this particular code base everything interacts with the DOM, window and the mobile clients rendering the WebView in which this JS is running. In other words, there’s nothing to unit test, unless you want to double the size of the code base just to add dependency inversion and dozens of mock implementations. Because of that, I decided to give Cypress a shot, because effortless end-to-end tests sounded like the perfect tool for this job.

On my MacBook Pro things worked perfectly. I have a little shell.nix which just sets up NodeJS. The project itself is built with Webpack and doesn’t use Nix at all. I can therefore use normal NPM commands, and so npm install cypress followed by ./node_modules/.bin/cypress open just works.

ENOENT

At home on my desktop machine I use NixOS though. And I knew that this wasn’t going to work because a project as complicated as Cypress surely uses a ton of global stuff, think dynamic linking and such. And indeed, the normal installation doesn’t work at all. The first thing that failed was just npm install, because a Cypress dependency had to be compiled from source. This was easy enough to fix by adding autoreconfHook to my Nix shell. This hook makes working with autotools a lot easier.

Now that Cypress was installed, the next step was actually running it. To my surprise, Cypress is actually packaged in Nixpkgs, but you won’t find it with the online search on the Nixpkgs website. I think it has something to do with this not being a top level package, but something in the web development section.

I tried this derviation with nix-shell -p cypress and it worked well. Unfortunately it greets you with a warning, that you’re not running Cypress in the officially intended way through NPM. Additionally, the Cypress version used in your project might now be different from what you’re actually using through Nix, which doesn’t sound like a good idea. Sure you can override the version in the derivation, provide a new sha256, maybe patch missing dependencies. But I really wanted to see if I can also run Cypress by just making its dependencies available in a Nix shell.

At first I tried adding the Cypress dependencies to my mkShell call. But that would always fail with a strange ENOENT error. At first I was a bit confused, because it didn’t tell me what was missing. But when you search for “Nix ENOENT” you’ll realize that this is because the filesystem on NixOS doesn’t conform to the Filesystem Hierarchy Standard (FHS).

Luckily, there’s a derivation called buildFHSUserEnv which does exactly that (here’s an excellent Stack Exchange answer with more info). I replaced mkShell with buildFHSUserEnv and tried again. This time I got an error message I could work with, namely that some dependency was missing (some shared object file). What now followed was a game of whack-a-mole where I would add the missing dependency to my Nix shell, run Cypress, watch it burn and add the next dependency.

What I ended up with is this beauty:

xorg.libXScrnSaver
xorg.libXdamage
xorg.libX11
xorg.libxcb
xorg.libXcomposite
xorg.libXi
xorg.libXext
xorg.libXfixes
xorg.libXcursor
xorg.libXrender
xorg.libXrandr
mesa
cups
expat
ffmpeg
libdrm
libxkbcommon
at_spi2_atk
at_spi2_core
dbus
gdk_pixbuf
gtk3
cairo
pango
xorg.xauth
glib
nspr
atk
nss
gtk2
alsaLib
gnome2.GConf
unzip
(lib.getLib udev)

That’s significantly more than the Cypress derivation in Nixpkgs uses. At first I thought it’s because I’m using 6.5.0 whereas Nixpkgs still has 6.0.0. But after opening a PR to update the version, which didn’t require any change in dependencies, I must admit that I have no idea why these requirements are so different. I must be missing something important here.

With the entire universe now being part of my Nix shell, things finally worked. At least sort of. Apparently lorri doesn’t work with buildFHSUserEnv. So now I need to call nix-shell manually.

This entire episode was a mixed bag of emotions. On the one hand I’m just glad I made it work. It feels good to know exactly what Cypress requires and that all of it will be garbage collected when I stop using it. On the other hand, this just works on my MacBook. Judging by their documentation, it’s not expected to work out of the box even on mainstream Linux distros.

This was another evening spent fixing something that shouldn’t be broken in the first place. On the other hand I learned a few things:

  • I memorized random words (libXdamage anyone?)
  • lorri does not heart buildFHSUserEnv
  • This will surely not work under Wayland

What About Docker

For CI I use a Docker file that runs Cypress in non-interactive mode. I actually tried using the same (official) Dockerfile together with X11. Here’s what I ended up calling:

$ docker run -it \
    -v (pwd):/e2e \
    -v /tmp/.X11-unix:/tmp/.X11-unix \
    -w /e2e \
    --entrypoint cypress \
    -h "$HOSTNAME" \
    --env DISPLAY="$DISLPAY" \
    cypress/included:6.6.0 open --project .

This doesn’t explode, but it also doesn’t do anything whatsoever. It just sits there. Once I hit CTRL+C I get an error about Cypress being killed unexpectedly. That makes me think that it does start and that it’s the X11 stuff that doesn’t work. But who knows, I didn’t want to debug this any further.

Final Nix Shell

For posterity, here’s the result. You’d want to make all of the buildFHSUserEnv conditional because it’s not needed and will likely fail on Darwin (MacOS).

let
  sources = import ./nix/sources.nix;

  pkgs = import sources.nixpkgs { };

in
(pkgs.buildFHSUserEnv {
  name = "cypress";

  targetPkgs = pkgs: (with pkgs; [
    xorg.libXScrnSaver
    xorg.libXdamage
    xorg.libX11
    xorg.libxcb
    xorg.libXcomposite
    xorg.libXi
    xorg.libXext
    xorg.libXfixes
    xorg.libXcursor
    xorg.libXrender
    xorg.libXrandr
    mesa
    cups
    expat
    ffmpeg
    libdrm
    libxkbcommon
    at_spi2_atk
    at_spi2_core
    dbus
    gdk_pixbuf
    gtk3
    cairo
    pango
    xorg.xauth
    glib
    nspr
    atk
    nss
    gtk2
    alsaLib
    gnome2.GConf
    unzip
    (lib.getLib udev)
    # Needed to compile some of the node_modules dependencies from source
    autoreconfHook
    autoPatchelfHook

    nodePackages.prettier
    nodePackages.eslint
    nodejs
  ]);
}).env