Skip to main content

Developing with Nix (C, JavaScript, Python, Haskell, Emscripten, PHP)

· 10 min read

Nix, NixOS and NixPkgs allows us to create project-specific development environments with project-specific dependencies (this usually means things like a C project, or a Python project... etc).

The way this is done is different for every language community within the Nix ecosystem. The most well developed patterns would be the C/C++, Haskell and Python community, other language communities tend to be smaller and has less documentation. This article serves as an introduction to using Nix for developing projects in different languages that we have worked with. This means we will only focus on shell.nix.

Remember that Nix is a turing complete purely functional language, and so it has real functions. Defining a project environment within Nix is done via functions. The most common function utilised for this purpose is stdenv.mkDerivation. However, various language communities within Nix has wrapped this function in higher order functions to provide further automation to deal with the environment configuration that different language toolchains expect.

First decide whether your project is an "application" or a "library". If it's an application, you need a default.nix and a shell.nix. But if it's a library you only need a shell.nix. The default.nix that is created may is generally not the same as the default.nix that gets committed into the mainline nixpkgs repository. Your project's default.nix should be a self-contained expression that can be used to build and install your project. Whereas the default.nix committed into the mainline nixpkgs needs to be compatible with the rest of the nixpkgs package set in order to merge into a "harmonious" package set. In practice this means the default.nix in your project will use a pinned nixpkgs package set, whereas the default.nix in nixpkgs will expect a pkgs parameter to be passed in.

The default.nix will be utilised when you run nix-build. The shell.nix will be utilised when you run nix-shell.

The shell.nix defines a development environment, so it is usually simpler than the default.nix. The default.nix needs to also define installation locations.

When encountering dependencies not available under nixpkgs, you have 2 choices. Either write a nix expression for that package, or utilise a language-specific package manager. In our examples below, we will utilise language-specific package managers within our shell.nix in order to create environments that work even under non-Nix environments. However, for dependencies that rely on system libraries, we'll generally install them via Nix, rather than doing compilation without or nix-shell. To make this work elegantly, we want to make sure we are not specifying dependencies twice, and some language-specific package managers are able to recognise dependencies already installed by Nix.

Nix 2.0 has the ability to verify hashes when using the builtins.fetchTarball function with the sha256 property. To know what hash you want to use. Run nix-prefetch-url --unpack https://package-you-want-to-use. In our examples, we utilise this parameter. However, to make the expressions work under 1.x, remove the sha256 parameter.

Often language specific dependencies do not appear in the online package search engine https://nixos.org/nixos/packages.html. To actually find available packages, you can either look at the nixpkgs source code, or use nix-repl with :l <nixpkgs> and then accessing the package set relevant to your language. The same package set may be aliased to multiple names. Some names are more general than other names. While the names may seem ambiguous, within a content-addressed package set, what these names map to is deterministic. In subsequent nixpkgs versions, the names may point to new package sets.

If you are making use of pinned nixpkgs following our previous tutorial: https://matrix.ai/2017/03/13/intro-to-nix-channels-and-reproducible-nixos-environment/ You can acquire the hash of your current pinning via: git -C /nix/nixpkgs rev-parse HEAD.

C

The package set for C in nixpkgs is itself!

Here is an example shell.nix for a C project:

{
pkgs ? import (fetchTarball {
url = https://github.com/NixOS/nixpkgs-channels/archive/084445b8f38ff8196f4b3f16d0ad0e79aa88dcbc.tar.gz;
sha256 = "0jqxx3csxbs32ijn8w6cbd9c3l9vvsjz57rqsyp44dgg37xxx00i";
}) {}
}:
with pkgs;
stdenv.mkDerivation {
name = "c-project";
buildInputs = [ autoreconfHook ];
}

Out of all the language environments, C has the best support. The stdenv.mkDerivation already brings in the standard C toolchain.

In this particular example, we've used autoreconfHook as a buildInput. You only need this if your project uses Autotools. If you use CMake, you would instead bring in cmake.

JavaScript

The package set for JavaScript in nixpkgs is: nodePackages.

Here is an example shell.nix for a JavaScript project:

{
pkgs ? import (fetchTarball {
url = https://github.com/NixOS/nixpkgs-channels/archive/00e56fbbee06088bf3bf82169032f5f5778588b7.tar.gz;
sha256 = "15pl5p3f14rw477qg316gjp9mqpvrr6501hqv3f50fzlcxn9d1b4";
}) {}
}:
with pkgs;
stdenv.mkDerivation {
name = "javascript-project";
buildInputs = [ nodejs-8_x flow ];
}

Note how we are utilising nodejs-8_x. This is just an alias. What the alias means depends on the content addressed package set. We are also bringing in flow which is a JavaScript type checker.

Within this project we can use npm freely just like normal. So that's where we will put all our dependencies.

For most projects, this is enough. However, some projects require building C++ extensions. To do this we need to find the toolchain required to build the extension. The 3 common choices are node-gyp, node-gyp-build and node-pre-gyp. Look at the package.json of the package you're trying to install to find out which one is being used. Once you know this, you can add the relevant toolchain to the buildInputs.

# you don't actually need all 3 gyp toolchains, this is just an example
buildInputs = [
nodejs-8_x
flow
nodePackages_8_x.node-gyp
nodePackages_8_x.node-gyp-build
nodePackages_8_x.node-pre-gyp
];

In some cases, you may also need python. It all depends on what kind of package you are installing.

The resulting project works in a Nix environment, and also works outside of a Nix environment.

Python

The package set for Python in nixpkgs is: python2Packages, python3Packages.

Here is an example shell.nix for a Python project:

{
pkgs ? import (fetchTarball {
url = https://github.com/NixOS/nixpkgs-channels/archive/630dbfe672164eeaf4bc822226cbb3ad7c1b0805.tar.gz;
sha256 = "0xvj0z928mzcs56hindxw608a6jm1fvsi2ap5r7m4a0plnrcwx90";
}) {}
}:
with pkgs;
python35Packages.buildPythonApplication {
name = "python-project";
buildInputs = (with python35Packages; [
numpy
gdal
scikitimage
tensorflowWithCuda
(matplotlib.override { enableQt = true; })
]);
shellHook = ''
echo 'Entering Python Project Environment'
set -v

# extra packages can be installed here
unset SOURCE_DATE_EPOCH
export PIP_PREFIX="$(pwd)/pip_packages"
python_path=(
"$PIP_PREFIX/lib/python3.5/site-packages"
"$PYTHONPATH"
)
# use double single quotes to escape bash quoting
IFS=: eval 'python_path="''${python_path[*]}"'
export PYTHONPATH="$python_path"
export MPLBACKEND='Qt4Agg'

set +v
'';
}

This one is more sophisticated because it shows how to get Tensorflow with CUDA, and matplotlib rendering with Qt4.

Because pip by default installs into a global directory, we have to set PIP_PREFIX to a Project local directory. This directory should then be ignored by .gitignore.

When using pip, it actually is aware of dependencies installed via Nix. So in this case, if you later try to install a package that depends on numpy, pip will not reinstall numpy. Unless of course that dependency has a constraint that is not met by the numpy that you installed. The point is you can still write a normal setup.py and requirements.txt, and the project is still usable by non-Nix developers.

Haskell

The package set for Haskell in nixpkgs is: haskellPackages.

Here is an example shell.nix for a Haskell project:

{
pkgs ? import (fetchTarball {
url = https://github.com/NixOS/nixpkgs-channels/archive/8b1cf100cd8badad6e1b6d4650b904b88aa870db.tar.gz;
sha256 = "1p0xxyz30bd2bg0wrfviqgsskz00w647h0l2vi33w90i42k8r3li";
}) {}
}:
with pkgs;
haskell.lib.buildStackProject {
name = "haskell-project";
buildInputs = [];
shellHook = ''
echo 'Entering Haskell Project Environment'
set -v

alias stack="\stack --nix"

set +v
'';
}

This example is really simple. Because here we are relying on stack to bring in all the Haskell dependencies similar to how we setup a shell.nix for JavaScript.

The usage of haskell.lib.buildStackProject wraps stdenv.mkDerivation and adds all these features: https://github.com/NixOS/nixpkgs/blob/master/pkgs/development/haskell-modules/generic-stack-builder.nix

When using --nix flag on stack, it just makes sure that stack will utilise the GHC supplied by the haskell.lib.buildStackProject, instead of acquiring its own GHC, which would be a waste of time. All other stack commands work normally.

Becareful with the usage of the alias here. If you have scripts (like a Makefile) inside that call stack, they may not have --nix applied. If so something will go wrong. It would be better to have some sort of environment variable instead, but I'm not aware of it.

An alternative to using the alias is to set:

nix:
enable: true

In your Project's stack.yaml or in ~/.stack/config.yaml. If you are expecting that your projects will be used by people not using Nix, then it makes sense not to put it in the Project's stack.yaml. However instead you can then put it in ~/.stack/config.yaml.

When using stack if a dependency doesn't build, that could be because it's missing a C library. The most common is probably zlib. If so, just put it into your buildInputs.

Emscripten

The package set for Emscripten in nixpkgs is: emscriptenPackages.

{
pkgs ? import (fetchTarball {
url = https://github.com/NixOS/nixpkgs-channels/archive/084445b8f38ff8196f4b3f16d0ad0e79aa88dcbc.tar.gz;
sha256 = "0jqxx3csxbs32ijn8w6cbd9c3l9vvsjz57rqsyp44dgg37xxx00i";
}) {}
}:
with pkgs;
stdenv.mkDerivation {
name = "emscripten-project";
buildInputs = [
emscripten
cmakeCurses
pkgconfig
python2
nodejs
flow
];
shellHook = ''
EMSCRIPTEN_ROOT_PATH='${emscripten}/share/emscripten'
EMSCRIPTEN='${emscripten}/share/emscripten'
'';
}

At this moment we are not aware of any function which wraps stdenv.mkDerivation that deals with emscripten requirements. Preferably this can either be stored in a package hook when you bring in emscripten or it can be placed into a special function. This means at this point if you bring in an emscriptenPackages, it isn't automatically registered. You have to find the relevant environment variable to configure in your shellHook.

PHP

The package set for PHP in nixpkgs is: phpPackages.

Here is an example shell.nix for a PHP project:

{
pkgs ? import (fetchTarball {
url = https://github.com/NixOS/nixpkgs-channels/archive/084445b8f38ff8196f4b3f16d0ad0e79aa88dcbc.tar.gz;
sha256 = "0jqxx3csxbs32ijn8w6cbd9c3l9vvsjz57rqsyp44dgg37xxx00i";
}) {}
}:
with pkgs;
stdenv.mkDerivation {
name = "php-project";
buildInputs = [
php71
] ++ (with php71Packages; [
composer
]);
}

Here we rely on composer to bring in all the PHP dependencies. So it's just like JavaScript.

Database Services

Application projects often might rely on a database service. You can use shell.nix shellHook to help set this up, so that each project can have its own database running within the shell. To make this work without adding in container/network namespaces, we rely on unix domain sockets. This allows each database process to only communicate with a project local unix domain socket.

Here is an example using MySQL and the Flyway migrations system:

shellHook = ''
echo 'Entering MySQL Environment'
. ./.env
set -v

alias mysql="\mysql --socket='$(pwd)/.mysql/mysql.sock' "$DB_DATABASE""
alias mysqladmin="\mysqladmin --socket='$(pwd)/.mysql/mysql.sock'"
alias mysqld="\mysqld \
--datadir="$(pwd)/.mysql" \
--socket="$(pwd)/.mysql/mysql.sock" \
--bind-address="$DB_HOST" \
--port="$DB_PORT""

alias flyway="\flyway \
-user="$DB_USERNAME" \
-password="$DB_PASSWORD" \
-url="jdbc:mysql://$DB_HOST:$DB_PORT/$DB_DATABASE" \
-locations=filesystem:$(pwd)/migrations";

set +v
'';

Notice the .env file which stores all the environment variables. So you can use shell.nix as a dotenv replacement.

Remember to set up initialise your database before you start using it!

rm -rf .mysql && mkdir .mysql
mysqld --datadir="$(pwd)/.mysql" --initialize-insecure

# launch your database without networking!
mysqld --skip-networking &

Again beware of using aliases, it is actually better to use environment variables when possible as they will carry over to any scripts you run.

Here's a bonus using PostgreSQL and PostGIS:

{
pkgs ? import (fetchTarball https://github.com/NixOS/nixpkgs-channels/archive/49a0ecc49b491c1f1a6f329d3d4fc9cf559b18aa.tar.gz) {}
}:
with pkgs;
let
pg = (pg:
buildEnv {
name = "postgresql-and-plugins-${(builtins.parseDrvName pg.name).version}";
paths = [ pg pg.lib (postgis.override { postgresql = pg; }) ];
buildInputs = [ makeWrapper ];
postBuild = ''
mkdir -p $out/bin
rm $out/bin/{pg_config,postgres,pg_ctl}
cp --target-directory=$out/bin ${pg}/bin/{postgres,pg_config,pg_ctl}
wrapProgram $out/bin/postgres --set NIX_PGLIBDIR $out/lib
'';
}
);
in
stdenv.mkDerivation {
name = "project-with-postgres-postgis";
buildInputs = [ (pg postgresql) ];
}