Sharing dependencies between default.nix and shell.nix#


What will you learn?#

In this tutorial you’ll learn how not to repeat yourself by sharing dependencies between default.nix, which is responsible for building the project, and shell.nix, which is responsible for providing you with an environment to work in.

How long will it take?#

This tutorial will take approximately 1 hour.

What will you need?#

This tutorial assumes you’re familiar with Nixpkgs build helpers (mkDerivation, buildPythonApplication, etc) and know how to create environments for nix-shell. While this tutorial uses Python as the language for the example project, no actual Python knowledge is requried.

Setting the stage#

Suppose you have a working build for your project in a default.nix file so that when you run nix-build it builds your project. It includes all of the dependencies needed to build it, but nothing more. Now suppose you wanted to bring in some tools during development, such as a linter, a code formatter, git commit hooks, etc.

One solution could be to add those packages to your build. This would certainly work in a pinch, but now your build depends on packages that aren’t actually required. A better solution is to add those development packages to a shell environment so that the build dependencies stay as lean as possible.

However, now you need to define a shell.nix that not only provides your development packages, but can also build your project. In other words, you need a shell.nix that brings in all of the packages that your build depends on. You could certainly copy the build dependencies from default.nix and copy them into shell.nix, but this is less than ideal: your build dependencies would be defined in two places. Maintaining duplicate declarations in default.nix and shell.nix opens the possibility for them to diverge, producing surprising results.

There is a better way!

Getting started#

Create a directory called shared_project and enter it:

$ mkdir shared_project
$ cd shared_project

You’ll be creating a Python web application as an example project, but don’t worry, you’ll be given all of the code you need and won’t need to know Python to proceed.

Create a new directory called src and two empty files inside of src called and

$ mkdir src
$ touch src/
$ touch src/

Copy the following contents into

1from flask import Flask
3app = Flask(__name__)
6def hello_world():
7    return "<p>Hello, World!</p>"

This creates a web application that returns <p>Hello, World!</p> on the / route.

Next create a pyproject.toml file with the following contents:

requires = ["setuptools", "setuptools-scm"]
build-backend = "setuptools.build_meta"

name = "shared_project"
version = "0.0.1"

app = "app:main"

This file tells Python how to build the project and what will execute when you run the executable called app.

For the Nix part of the project you’ll create two files: package.nix and default.nix. The actual build recipe will be in package.nix and default.nix will import this file to perform the build.

First create a package.nix file like this:

 2  buildPythonApplication,
 3  setuptools-scm,
 4  flask,
 7buildPythonApplication {
 8  pname = "shared_project";
 9  version = "0.0.1";
10  format = "pyproject";
11  src = builtins.path { path = ./.; name = "shared_project_source"; };
12  propagatedBuildInputs = [
13    setuptools-scm
14    flask
15  ];

The Nix expression in this file is a function that produces a derivation. This method of defining builds is a common design pattern in the Nix community, and is the format used throughout the nixpkgs repository. This particular derivation builds your Python application and ensures that flask, the library used to create the web application, is available at runtime.

Note that on line 11 of the package.nix file the src attribute is set using builtins.path. This creates a reproducible source path, and is a good habit to form.

Finally, create a default.nix that looks like this:

2  pkgs = import <nixpkgs> {};
4  {
5    build = pkgs.python3Packages.callPackage ./package.nix {};
6  }

The python3Packages.callPackage function determines which arguments the function in package.nix takes (in this case, buildPythonApplication, setuptools-scm, and flask) then calls the function in package.nix with the corresponding attributes from python3Packages. You can read more about the callPackage pattern in the Nix Pills.

Also note that this default.nix returns an attribute set with a single attribute called build. This allows adding more attributes later without breaking existing consumers. Try to build this project by running nix-build -A build

Adding development packages#

As mentioned earlier, you’ll want to add some development packages. Edit default.nix to look like this:

 2  pkgs = import <nixpkgs> {};
 3  build = pkgs.python3Packages.callPackage ./package.nix {};
 5  {
 6    inherit build;
 7    shell = pkgs.mkShell {
 8      inputsFrom = [ build ];
 9      packages = with pkgs.python3Packages; [
10        black
11        flake8
12      ];
13    };
14  }

Let’s break this all down.

The pkgs.mkShell function produces a shell environment, and it’s common to put the expression that calls this function in a shell.nix file by itself. However, doing so means that you to declare pkgs = ... a second time (first in default.nix, then again in shell.nix) and if you’re pinning nixpkgs to a particular revision you may forget to update one of the declarations.

By putting the build declaration in the let binding on line 3 you’re able to use it throughout the attribute set that spans lines 5-14. Line 6 includes the build attribute in the attribute set. Lines 7-13 produce the shell environment for working on the project.

The real magic is the inputsFrom attribute passed to mkShell on line 8, which allows you to include build inputs from other derivations in your shell. This is what allows you to not repeat yourself.

Finally, the packages attribute passed to mkShell is where you list any executable packages you’d like to be available in your shell.

Now create a shell.nix file with the following contents:

1(import ./default.nix).shell

Since default.nix produces an attribute set, the shell.nix file is able to evaluate default.nix and simply access the shell attribute.

Now you can build the project by running nix-build -A build and you can enter the shell simply by running nix-shell.

Testing out the shell#

Enter the shell with the nix-shell command, then verify that you have the flake8 and black programs available:

$ nix-shell
...lots of output from the build
$ which flake8
$ which black

These are the Nix store paths on the author’s machine at the time of writing. You will likely see different store paths and versions depending on when you execute these commands and the architecture of the machine that the commands are executed on.

Next steps#