7.4 KiB
Sharing dependencies between default.nix
and shell.nix
Overview
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've seen a derivation (mkDerivation
, buildPythonApplication
, etc) before, and that you've seen nix-shell
used to create shell environments.
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 necessary for it to actually build. 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 are defined in multiple places, and aside from repeating yourself there's now the possiblity that the dependencies in default.nix
and shell.nix
may fall out of sync.
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 __init__.py
and app.py
:
$ mkdir src
$ touch src/__init__.py
$ touch src/app.py
Copy the following contents into app.py
:
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello_world():
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:
[build-system]
requires = ["setuptools", "setuptools-scm"]
build-backend = "setuptools.build_meta"
[project]
name = "shared_project"
version = "0.0.1"
[project.scripts]
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: build.nix
and default.nix
.
The actual build recipe will be in build.nix
and default.nix
will import this file to perform the build.
First create a build.nix
file like this:
{
buildPythonApplication,
setuptools-scm,
flask,
}:
buildPythonApplication {
pname = "shared_project";
version = "0.0.1";
format = "pyproject";
src = builtins.path { path = ./.; name = "shared_project_source"; };
propagatedBuildInputs = [
setuptools-scm
flask
];
}
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 for the application.
Note that on line 11 of the build.nix
file the src
attribute is set using builtins.path
.
This is a good habit to form because it will give your build a fixed name rather than simply inheriting the name of the parent directory.
Finally, create a default.nix
that looks like this:
let
pkgs = import <nixpkgs> {};
in
{
build = pkgs.python3Packages.callPackage ./build.nix {};
}
The callPackage
function reads the expression in build.nix
to determine which inputs it needs (in this case, buildPythonApplication
, setuptools-scm
, and flask
), then calls the expression with the inputs that were requested.
You can read more about callPackage
in the Nix Pills.
Also note that this default.nix
returns an attribute set with a single attribute called build
.
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:
let
pkgs = import <nixpkgs> {};
build = pkgs.python3Packages.callPackage ./build.nix {};
in
{
inherit build;
shell = pkgs.mkShell {
inputsFrom = [build];
packages = with pkgs.python3Packages; [
black
flake8
];
};
}
Now create a shell.nix
file with the following contents:
(import ./default.nix).shell
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 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.
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
/nix/store/vmp3jii75jqmi7vi9mg3v9ackal6wl4i-python3.10-flake8-6.0.0/bin/flake8
$ which black
/nix/store/q9vw01b2jz8h7kjq603hs3lz90i4d6d8-python3.10-black-23.1.0/bin/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.