Using Debian Packages on NixOS

Intro and Motivation

The NixOS Linux distribution takes its packages from the nixpkgs project: this packages up a whole bunch of software for installation, but more than that it provides a flexible architecture for defining our own packages, overriding the defaults (e.g. applying patches, using different dependencies, etc.). This is great, and pretty extensive, but it doesn’t include everything we might want.

Most of the time, if something we want isn’t available in nixpkgs then it’s pretty easy to make a package: if it’s a Haskell program we might use cabal2nix, if it’s Python we might use buildPythonPackage, if it’s C we might use stdenv.mkDerivation, and so on.

Yet there are some occasions where this isn’t the best route to take. In my case, I wanted a particular version of the Chromium browser, and whilst Chromium is available in nixpkgs, my system’s customisations caused it to require compilation (Nix fetches pre-built packages from a ‘binary cache’, but there obviously won’t be a pre-built version available which has my personal customisations). Compiling Chromium is notoriously resource-intensive, so I wanted to avoid this if possible; I could fetch an unmodified version of the package, but I wanted to try a different approach and use the version from Debian instead.

The Idea

We’re going to make a Nix package which contains a small Debian installation, and run our application (Chromium) via chroot. This is like a “poor man’s container”, but we’ll be using Nix to manage all of the files and scripts rather than something like Docker.

Since we want all of this to be usable by an unprivileged user account, we’ll also be using proot rather than chroot, since chroot requires root privileges (in contrast, proot works by intercepting system calls).

The Setup

There are two approaches we can take to getting Debian set up: one is to use debootstrap, which will download and install the required packages into a directory. The other is to use a pre-built version (basically an archive of the directory made by debootstrap). Since we’re aiming to use pre-built binaries, I opted to also use a pre-built filesystem image.

Here’s a Nix expression forgetting a Debian filesystem image, which we take from a collection of Docker resources:

{ fetchurl }:
rec {
  rootVersion = "67a0101a76eed558d4b61a484a27c9f9d7a119f4/stretch";

  rootRepo = "debuerreotype/docker-debian-artifacts";

  rootfs = fetchurl {
    url    = "https://github.com/${rootRepo}/raw/${rootVersion}/rootfs.tar.xz";
    sha256 = "1ff2qjvfj6fbwwj17wgmn3a4mlka1xv1p3jyj465dbf4qf3x0ijm";
  };
}

If you’re following along at home, you can save the above to a file like debian-rootfs.nix and play around with it, e.g. in nix-repl, like this:

nix-repl> with import <nixpkgs> {}; callPackage ./debian-rootfs.nix {}

OK, now that we have a Debian filesystem image, we’ll need to unpack it and add in the changes we need (e.g. installing Chromium). I’ll do this in one go, to prevent cluttering up our Nix store with intermediate result. The approach we take is to write all of our customisations in a shell script, then run that shell script within the Debian environment using proot. Note that Nix packages get built by unprivileged users (usually called something like nixbld), and without access to an interactive terminal (since that would be impure), which is why we use proot rather than chroot:

{ cacert, callPackage, proot, runCommand, writeScript }:
with rec {
  inherit (callPackage ./debian-rootfs.nix {}) rootfs;

  # See https://github.com/proot-me/PRoot/issues/106
  PROOT_NO_SECCOMP = "1";
};
runCommand "debian-with-chromium"
  {
    inherit rootfs PROOT_NO_SECCOMP;
    buildInputs      = [ proot ];
    SSL_CERT_FILE    = "${cacert}/etc/ssl/certs/ca-bundle.crt";
    script           = writeScript "setup.sh" ''
      #!/usr/bin/env bash
      set -e
      apt-get update
      apt-get install -y chromium
      chmod 4755 /usr/lib/chromium/chrome-sandbox
    '';
  }
  ''
    echo "Unpacking Debian" 1>&2
    mkdir "$out"
    pushd "$out"
      tar xf "$rootfs"
    popd

    echo "Installing setup script" 1>&2
    cp "$script" "$out/setup.sh"

    echo "Pointing PATH to Debian binaries" 1>&2
    export PATH="/bin:/usr/bin:/sbin:/usr/sbin:$PATH"

    echo "Resetting /tmp variables" 1>&2
    export TMPDIR=/tmp
    export TEMPDIR=/tmp
    export TMP=/tmp
    export TEMP=/tmp

    echo "Setting up" 1>&2
    proot -r "$out" -b /proc -b /dev -0 /setup.sh
  ''

If we save this to a file like debian-with-chromium.nix (in the same directory as debian-rootfs.nix, or else adjust the path given to callPackage), then we can build the package with a command like:

nix-build --show-trace debian-with-chromium.nix

So far so good, but this doesn’t actually let us run Chromium.

Using the Debian Environment

To run a command in this Debian environment, we’ll again use proot. This time we could use sudo chroot, if our user is privileged, but I think that’s overly restrictive (what about non-privileged accounts?), too much hassle (typing in passwords), and has a larger scope to go wrong (sudo gives full system access).

I’ll make use of the wrap helper function defined in my nix config. Again, save this to a file like debian-chromium.nix, in the same directory as debian-with-chromium.nix (or adjust paths appropriately):

{ bash, callPackage, proot, wrap }
wrap {
  name   = "chromium-exec";
  paths  = [ bash proot ];
  vars   = {
    env = callPackage ./debian-with-chromium.nix {};

    # See https://github.com/proot-me/PRoot/issues/106
    PROOT_NO_SECCOMP = "1";
  };
  script = ''
    #!/usr/bin/env bash
    export PATH="/bin:/usr/bin:/sbin:/usr/sbin:$PATH"
    export TMPDIR=/tmp
    export TEMPDIR=/tmp
    export TMP=/tmp
    export TEMP=/tmp

    # shellcheck disable=SC2154
    proot -r "$env" -b /proc -b /dev -b /nix -b /tmp -b /home "$@"
  '';
}

Now we can launch Chromium. Since wrap outputs a fully self-contained script, we can just call that from our usual shell:

$(nix-build --show-trace \
            -E '(import <nixpkgs> {}).callPackage ./debian-chromium.nix {}')

If you want to add the resulting script to your PATH then you could use mkBin instead of wrap. The result can be installed just like any other Nix package, either system-wide (if we have permission), or just to our user profile, or just ad-hoc via nix-shell.

Happy hacking :)