
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.
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) ];
}

