Every Python package (which is a collection of modules associated with version
metadata) needs to have a setup.py.
So first create a relevant setup.py, refer to my article here:
https://gist.github.com/CMCDragonkai/f0285cdc162758aaadb957c52f693819 for more
information.
Remember that setup.py is meant to store version compatible ranges of
dependencies, whereas requirements.txt is an optional fixed package set list
generated by pip freeze > requirements.txt.
It is incorrect to use use requirements.txt as the install_requires list in
setup.py because they are not meant to be used for the same thing.
The requirements.txt is optional, but is a good practice for allowing non-Nix
users to attempt to build a reproducible environment.
However because we install the current package in --editable mode, then you
should remove the line referencing the current package from the requirements.txt.
After that we need pkgs.nix, default.nix, shell.nix and release.nix.
The pkgs.nix will point to a content address Nixpkgs distribution. The
default.nix will be the derviation of the current package, and work with
nix-build. The shell.nix will import default.nix and override with
development specific dependencies. Finally release.nix will import
default.nix and produce several release targets. One of them will be Docker.
You should also have a MANIFEST.in that includes all the Nix files.
The following is an example of the relevant files.
The setup.py:
#!/usr/bin/env python3
from setuptools import setup, find_packages
with open('README.md', 'r') as f:
long_description = f.read()
setup(
name='awesome-package',
version='0.0.1',
author='Your Name',
author_email='your@email.com',
description='Does something awesome',
long_description=long_description,
url='https://awesome-package.git',
packages=find_packages(),
scripts=['awesome-script'],
install_requires=['numpy'])The pkgs.nix:
import (fetchTarball https://github.com/NixOS/nixpkgs-channels/archive/e6b8eb0280be5b271a52c606e2365cb96b7bd9f1.tar.gz) {}The default.nix:
{
pkgs ? import ./pkgs.nix,
pythonPath ? "python36"
}:
with pkgs;
let
python = lib.getAttrFromPath (lib.splitString "." pythonPath) pkgs;
in
python.pkgs.buildPythonApplication {
pname = "awesome-package";
version = "0.0.1";
src = lib.cleanSourceWith {
filter = (path: type:
! (builtins.any
(r: (builtins.match r (builtins.baseNameOf path)) != null)
[
"pip_packages"
".*\.egg-info"
])
);
src = lib.cleanSource ./.;
};
propagatedBuildInputs = (with python.pkgs; [
numpy
]);
}The shell.nix:
{
pkgs ? import ./pkgs.nix,
pythonPath ? "python36"
}:
with pkgs;
let
python = lib.getAttrFromPath (lib.splitString "." pythonPath) pkgs;
drv = import ./default.nix { inherit pkgs pythonPath; };
in
drv.overrideAttrs (attrs: {
src = null;
shellHook = ''
echo 'Entering ${attrs.pname}'
set -v
# extra pip packages
unset SOURCE_DATE_EPOCH
export PIP_PREFIX="$(pwd)/pip_packages"
PIP_INSTALL_DIR="$PIP_PREFIX/lib/python${python.majorVersion}/site-packages"
export PYTHONPATH="$PIP_INSTALL_DIR:$PYTHONPATH"
export PATH="$PIP_PREFIX/bin:$PATH"
mkdir --parents "$PIP_INSTALL_DIR"
pip install --editable .
set +v
'';
})The release.nix:
{
pkgs ? import ./pkgs.nix,
pythonPath ? "python36"
}:
with pkgs;
let
drv = import ./default.nix { inherit pkgs pythonPath; };
in
{
docker = dockerTools.buildImage {
name = drv.pname;
contents = drv;
config = {
Cmd = [ "/bin/awesome-script" ];
};
};
}The MANIFEST.in:
include README.md
include LICENSE
include requirements.txt
include pkgs.nix
include default.nix
include release.nix
include shell.nix
graft tests
global-exclude *.py[co]
When using buildPythonApplication or buildPythonPackage, both expect that the
source is a legitimate Python package (a directory that contains a setup.py).
The difference between buildPythonApplication and buildPythonPackage is that
the buildPythonApplication does not prefix the resulting derivation name with
the Python interpreter major version. Therefore buildPythonApplication should
be used when the derivation is intended for applications that are not used like
libraries (for example if they include scripts). However there are packages
that are used like applications and libraries. In that case refer to the
Nixpkgs manual and look for toPythonApplication documentation.
When using buildPythonApplication or buildPythonPackage make sure to use
pname and not name. The name will be automatically created by joining the
pname and version.
The numpy package is put into propagatedBuildInputs instead of buildInputs
because Python is an interpreted language, which requires numpy package to
exist at runtime.
The usage of lib.cleanSource and lib.cleanSourceWithis in order to ignore
metadata files and directories such as .git along with a number of generated
files such as the./result symlink created by nix-build. This avoids
"store-leak" caused by repeated invocations of nix-build under the presence
of changes to the src path. For more information see:
https://gist.github.com/CMCDragonkai/8d91e90c47d810cffe7e65af15a6824c
Note the override of src used in the shell.nix because the nix-shell
environment does not need create a temporary build directory. It just uses your
current directory.
The unsetting of SOURCE_DATE_EPOCH is needed for building binary wheels.
We use pip_packages for pip installed local packages. This is meant to match
node_packages used in Node.js. It makes it easier to delete if we need to
wipe it out and install it again.
Note how pip install --editable . ensures that within the development
environment, module directories are "importable" using absolute names. It also
means that updates to the Python source code will be automatically used by any
installed scripts that are now in the $PATH environment variable.
Note that pip will ignore any dependencies that are already installed by Nix.
Sometimes you want to force pip to ignore installation of any dependencies,
especially when you already have all the dependencies required, and pip with
setuptools does dumb things. In those cases, use:
installFlags = [ "--no-deps" ];When doing this, you will probably also need to disable tests:
doCheck = false;Remember to add /result to your .gitignore to deal with nix-build.
This allows your whole project to be used by developers who are not using Nix, and you can submit it to PyPi.
Sometimes you need to override a package in the package set for all dependencies.
This can happen due to dependency collision:
Package duplicates found in closure, see above. Usually this happens if two packages depend on different version of the same dependency.
A common example is setting the backend of matplotlib to Qt4.
To do this, create an overrides.nix like this:
{ pkgs, pythonPath }:
with pkgs;
let
pythonPath_ = lib.splitString "." pythonPath;
in
import path
{
overlays = [(
self: super:
lib.setAttrByPath
pythonPath_
(
lib.getAttrFromPath (pythonPath_ ++ ["override"]) super
{
packageOverrides = self: super:
{
matplotlib = super.matplotlib.override { enableQt = true; };
};
}
)
)];
}Then in default.nix change to using pkgs_ instead of pkgs:
pkgs_ = import ./overrides.nix { inherit pkgs pythonPath; };The main reason this is required for Python, is that Python doesn't have the ability to keep multiple versions of the same dependency in the same project. Python's transitive dependencies must use the same dependency version for all their dependencies.
Install into Nix user profile:
nix-env -f ./default.nix -iUsing Docker:
# load the container from serialised image
docker load --input "$(nix-build ./release.nix --attr docker)"
# run the container
docker run awesome-package
# run the container with alternative command (works even with no Cmd)
docker run awesome-package /bin/some-other-command
# view contents
docker save awesome-package | tar x --to-stdout --wildcards '*/layer.tar' | tar t --exclude="*/*/*/*"# explore the nixpkgs package set
nix-repl ./pkgs.nix
# development environment (if there are multiple attributes, one must be chosen with --attr)
nix-shell
# you can also use nix-shell against a different package set
nix-shell -I nixpkgs=/some/other/nixpkgs -p "python3.withPackages (ps: with ps; [ dask distributed ])"
nix-shell -I nixpkgs=https://github.com/NixOS/nixpkgs-channels/archive/06808d4a14017e4e1a3cbc20d50644549710286f.tar.gz -p "python3.withPackages (ps: with ps; [ dask distributed ])"
# test the build (if there are multiple attributes, all will be built as ./result, ./result-*)
nix-build
# check the derivation
nix-instantiate
# show immediate dependencies (derivations)
nix-store --query --references $(nix-instantiate)
# show transitive dependencies (derivations)
nix-store --query --requisites $(nix-instantiate)
# clean the build
link="$(readlink ./result)" \
&& rm ./result \
&& nix-store --delete "$link"
# show the derivation closure in JSON
nix show-derivation --file ./default.nix
# show the derivation closure recursively in JSON
nix show-derivation --file ./default.nix --recursive
# show how your derivation depends on another derivation
nix why-depends \
--include 'pkgs=./pkgs.nix' \
--include 'default=./default.nix' \
--all \
default \
pkgs.python36.pkgs.numpyYou can also use nix-gc to help clean up result symlinks: https://gist.github.com/CMCDragonkai/0908706df9c9dbc45575a2345fab93f1
Note the reason why this doesn't use pypi2nix is because pypi2nix doesn't
share derivations/store-paths with the nixpkgs pythonPackages package set.
This is not ideal. However if you really need to work on a custom Python package
that has lots of packages not packaged in nixpkgs, then you can use pypi2nix
instead. nix-community/pypi2nix#222
Dealing with data files is tricky in Python. Basically you have 3 options:
- Using
package_data - Using
data_files - Using
MANIFEST.in
https://blog.ionelmc.ro/presentations/packaging
When using package_data, the files have to be inside a Python module directory.
Once installed, they will sit in the installed Python module directory. To properly
refer to these files in the module directory you have to use pkg_resources module
which is provided by the setuptools package. Which means setuptools becomes a
runtime dependency, which is not very nice. In python 3.7 we now have importlib.resources.
If that file is only loaded by module code within in the same directory, it is sufficient to
use this instead: https://gist.github.com/CMCDragonkai/2e0ae76e87537b708ed12ba05851d96b
Note that you must use the name of the module directory:
package_data={
'module_dir': ['file_in_module_dir']
}See: https://importlib-resources.readthedocs.io/en/latest/
The data_files specification is designed to refer to files that aren't actually meant
to be used from within Python. However it's also incredibly unreliable, as there are
lots of ways of installing Python packages, and all of them may install them in different ways.
For Nix, you can expect them to exist relative to the Nix store output path. You may use
data_files for when you are building things that also involve non-Python code that
expect things like man pages or things in the share directory.
Finally there are files that you expect to exist in the source distribution but not
in the final build. You need to specify these files in the MANIFEST.in. These
files are basically things like licenses, source documentation, tests... etc.