Using Nix for Repeatable Python Environments
Daniel Wheeler
github.com/wd15
Overview
- Packaging concerns
- What is Nix?
- How can you use Nix?
- Nix language tutorial
- Python examples, tips and tricks
- How to fix Nix for Python
Packaging Concerns
$ conda activate my-clean-env
$ python --version
Python 3.7.3
$ conda install sfepy
...
$ python --version
Python 2.7.15
$ conda deactivate
$ conda activate my-new-clean-env
$ python --version
Python 3.6.7
$ conda install sfepy
...
$ python --version
Python 3.6.7
My PYTHON VERSION CHANGED!
$ conda install fipy python=3.6
...
$ python -c "import numpy; numpy.__config__.show()"
blas_mkl_info:
libraries = ['mkl_rt', 'pthread']
...
$ conda install sfepy python=3.6
...
$ python -c "import numpy; numpy.__config__.show()"
blas_mkl_info:
libraries = ['mkl_rt', 'pthread']
...
$ source activate clean-env
$ conda install sfepy python=3.6
...
$ python -c "import numpy; numpy.__config__.show()"
blas_mkl_info:
NOT AVAILABLE
...
$ conda install fipy python=3.6
...
$ python -c "import numpy; numpy.__config__.show()"
blas_mkl_info:
NOT AVAILABLE
...
Inconsistent outcomes
outcomes are dependent on state!
rollbaCKS
$ conda activate my-clean-env
$ python --version
Python 3.7.3
$ conda install fipy
...
$ python --version
Python 3.7.3
$ conda install sfepy
...
$ python --version
Python 2.7.15
$ conda remove sfepy
...
$ python --version
Python 2.7.15
No rollback capability in most package managers
dIAMOND dEPENDENCY
FiPy
Sfepy
Openblas
Numpy
Blas (MKL)
myenv
Questions
- Why isn't it safe to update my environment?
- Why can't I rollback to a previous environment?
- Why can't I build with multiple versions of the same library?
- Why is testing with different configurations so difficult?
Why isn't package management more like version control?
What is Nix?
Nix*
- Nix (package manager)
- Nix (language)
- Nixpkgs (collection of Nix package recipes)
- NixOS (linux distro)
- Other Nix jargon:
- NixOps (tools for deploying Nix and NixOS)
- DisNix (tools for deploying services (like Ansible))
- Hydra CI system
- Guix (GNU form of Nix, based on Scheme)
- Cachix (Cache your Nix builds)
How does nIX Work?
Nixpkgs
NIx Package Manager
Nix Expression
Derivation
/nix/store
Instantiate
Realize
Impure, but hashed
default.nix
SHA
Derivation is independent of /nix/store
/nix/store/n1abx6l20rq73h8k84lpcysnm2d33jz2-python3.6-numpy-1.14.0
/nix/store/9w1m9b1zqa2k64pdaj20c39ay0vj2gc4-gfortran-7.3.0-lib
"NixOS: A Purely Functional Linux Distribution"
In this article we show that we can overcome these problems by moving to a purely functional system configuration model. This means that all static parts of a system (such as software packages,configuration files and system startup scripts) are built by pure functions and are immutable, stored in a way analogous to a heap in a purely functional language.
purely functional data structure (immutable)
Dolstra et al.
repology.org/statistics
NixOS
Nix Tutorial
nix-repl> 1 + 2 # Integers
3
nix-repl> 1 / 3. # Floats
0.333333
nix-repl> builtins.typeOf { a = 1; } # Sets
"set"
nix-repl> { a = 1; b = 2; }.b
2
nix-repl> rec { a = 2; b = a; }
{ a = 2; b = 2; }
nix-repl> ./hello_world.txt # Paths
/home/wd15/hello_world.txt
nix-repl> f = x: x * x
nix-repl> f 2 # purely functional (expressions and lazy)
4
The BASics
nix-repl> data = builtins.readFile ./hello_world.txt # read from a file
nix-repl> data
"Hello World!\n"
nix-repl> builtins.toFile "foo.txt" data # write to a file
"/nix/store/mm4fq57cw5gkjgn1c2yysgrq6cd1w6z2-foo.txt"
nix-repl> builtins.toFile "foo.txt" (data + "foo")
"/nix/store/xshshqgxxp9hsgrf8a7k229vw5f2rj9q-foo.txt"
ImPURE CheatING
Can Only write to the /nix/store!
Read and write files
Two versions of the same file
nix-repl> :l <nixpkgs>
Added 10482 variables.
nix-repl> d = builtins.derivation {
name = "my-hello";
builder = "${bash}/bin/bash";
args = [ ./build.sh ];
src = ./my-hello;
system = builtins.currentSystem; }
nix-repl> d
«derivation /nix/store/9d80ryw1lanalq4avvjh2hjd7sckid27-my-hello.drv»
Instantiate
A Derivation
{
"/nix/store/9d80ryw1lanalq4avvjh2hjd7sckid27-my-hello.drv": {
"outputs": {
"out": {
"path": "/nix/store/ka2vfi17abpi2np3i5xikjjdymd5wldf-my-hello"
}
},
"inputSrcs": [
"/nix/store/r3wkhhmj6rn3hpm04cw6yd9vc9yy9bhp-build.sh",
"/nix/store/y8iplxzxh00i32iiz4pl1zw8g95pfvh0-my-hello"
],
"inputDrvs": {
"/nix/store/wlmr9cbm9zsn6bxx204zpxpbpwg7vxiq-bash-4.4-p23.drv": [
"out"
]
},
"platform": "x86_64-linux",
"builder": "/nix/store/r47p5pzx52m3n34vdgqpk5rvqgm0m24m-bash-4.4-p23/bin/bash",
"args": [
"/nix/store/r3wkhhmj6rn3hpm04cw6yd9vc9yy9bhp-build.sh"
],
"env": {
"builder": "/nix/store/r47p5pzx52m3n34vdgqpk5rvqgm0m24m-bash-4.4-p23/bin/bash",
"name": "my-hello",
"out": "/nix/store/ka2vfi17abpi2np3i5xikjjdymd5wldf-my-hello",
"src": "/nix/store/y8iplxzxh00i32iiz4pl1zw8g95pfvh0-my-hello",
"system": "x86_64-linux"
}
}
}
Python Examples, Tips and Tricks
Imperative Installation
Nix supports imperative installation
$ nix-env -i hello
installing ‘hello-2.10’
...
$ hello
Hello, world!
$ nix-env -q
hello-2.10
$ nix-env --list-generations
1 2019-06-20 13:56:08
$ nix-env -i python
$ nix-env -q
hello-2.10
python-2.7.16
$ nix-env --list-generations
1 2018-08-15 10:46:38
2 2018-08-15 10:47:22 (current)
$ nix-env --rollback
switching from generation 2 to 1
$ nix-env --list-generations
1 2019-06-20 13:56:08 (current)
2 2019-06-20 13:57:39
$ nix-env -q
hello-2.10
BUT NOT FOR PYTHON
Need to use nix-shell
$ nix-shell --pure -p python27 -p python27Packages.numpy -p python27Packages.scipy
these paths will be fetched (52.90 MiB download, 245.07 MiB unpacked):
...
[nix-shell] $ echo $PATH
/nix/store/qz40q38lask9zx6h0rgwf1qbcllclx82-bash-interactive-4.4-p23/bin:/nix/store/qpzwm6z4igakmqr4n4k6k3q0a4bqy3ws-patchelf-0.9/bin:/nix/store/1ap5d85630s3ksal6xgkjnbglmbng3kg-gcc-wrapper-7.4.0/bin:/nix/store/d8k7bv2w9g669dv7r9z4wrr9cnzdncdv-gcc-7.4.0/bin:/nix/store/2fxzw4ilrgc4klppk1nc50vgcwfphh1s-glibc-2.27-bin/bin:/nix/store/wpjdad5wpylnpqbjw4dbnih8f6q32l43-coreutils-8.31/bin:/nix/store/yzijh65sak6z06cdvrg9wi2d1964g6h3-binutils-wrapper-2.31.1/bin:/nix/store/c222w06ysx899n7r1jqaw96l6x6g8q9i-binutils-2.31.1/bin:/nix/store/2fxzw4ilrgc4klppk1nc50vgcwfphh1s-glibc-2.27-bin/bin:/nix/store/wpjdad5wpylnpqbjw4dbnih8f6q32l43-coreutils-8.31/bin:/nix/store/9yi3j41iwz045cn8h6brghi3lzsd886k-python-2.7.16/bin:/nix/store/7fr8a5qqyhlws3cnklnr37qywxdrihx3-python2.7-numpy-1.16.3/bin:/nix/store/9yi3j41iwz045cn8h6brghi3lzsd886k-python-2.7.16/bin:/nix/store/ssarsbjq5w5r4rqb37n24kg8h6q0nc0c-python2.7-setuptools-41.0.1/bin:/nix/store/wpjdad5wpylnpqbjw4dbnih8f6q32l43-coreutils-8.31/bin:/nix/store/d9y878m6hk96mc05pz4vdzqrlqcfl7rs-findutils-4.6.0/bin:/nix/store/xnvj5phwyv5fj4idkrwass1243nn5n19-diffutils-3.7/bin:/nix/store/g92ybkzhiqcw7xsz5yq1dayjjlhll914-gnused-4.7/bin:/nix/store/da2jifip9xsab81yh34h887a1fwnz9gd-gnugrep-3.3/bin:/nix/store/vk9rvr21phkc2jfb765rnrlhb171ywh2-gawk-4.2.1/bin:/nix/store/d34nmar6fd15yf24rw8h5aiiwcywlzbq-gnutar-1.32/bin:/nix/store/f1689ai31ca0m7j6q8lpyf10z16rmrmw-gzip-1.10/bin:/nix/store/cr2fv6r0bic97wmakjdxcwbvyp4hqn2z-bzip2-1.0.6.0.1-bin/bin:/nix/store/cpn4i0g3s7m5l0i6v1i7k855ylfxfn0g-gnumake-4.2.1/bin:/nix/store/fyxcppddjg7abrand8n10gwzm5gknc48-bash-4.4-p23/bin:/nix/store/1nnnsl2l0hcnzcy4410pbpicsvkfkzj9-patch-2.7.6/bin:/nix/store/rshc59lkp5j44fjzg2a5hi2ril4dd7ka-xz-5.2.4-bin/bin
[nix-shell] $ echo $PYTHONPATH
/nix/store/9yi3j41iwz045cn8h6brghi3lzsd886k-python-2.7.16/lib/python2.7/site-packages:/nix/store/7fr8a5qqyhlws3cnklnr37qywxdrihx3-python2.7-numpy-1.16.3/lib/python2.7/site-packages:/nix/store/ssarsbjq5w5r4rqb37n24kg8h6q0nc0c-python2.7-setuptools-41.0.1/lib/python2.7/site-packages:/nix/store/4kkq89k2gag7fq9kr1gfhp3vvcfbhzwc-python2.7-scipy-1.2.1/lib/python2.7/site-packages
- Each package installed in a separate site-packages directory
-
Requires new hash if installed into base site-packages
Completely pure environment
Repeatable environment
# shell.nix
let
nixpkgs = import <nixpkgs> {};
pypkgs = nixpkgs.python36Packages;
in
(pypkgs.python.withPackages (x: [ x.numpy x.scipy ])).env
$ nix-shell -I nixpkgs=https://github.com/NixOS/nixpkgs/archive/6a3f5bcb061e1822f50e299f5616a0731636e4e7.tar.gz
Script the previous nix-shell command
and maybe even specify the version of nixpkgs
Specify NIXPKGs Version
# Usage:
# nixpkgs = import nixpkgs_version.nix;
#
let
inherit (import <nixpkgs> {}) fetchFromGitHub;
nixpkgs_download = fetchFromGitHub {
owner = "NixOS";
repo = "nixpkgs-channels";
rev = "61deecdc34fc609d0f805b434101f3c8ae3b807a"; # release version: 18.09-beta
sha256 = "147xyn8brvkfgz1z3jbk13w00h6camnf6z0bz0r21g9pn1vv7sb0";
};
in
import nixpkgs_download {}
Fully repeatable by enforcing the Nixpkgs version
Prevent Version Drift!!!
buildPythonPackage
pypkgs.buildPythonPackage rec {
pname = "scipy";
version = "0.19.1";
src = pypkgs.fetchPypi {
inherit pname version;
sha256 = "1rl411bvla6q7qfdb47fpdnyjhfgzl6smpha33n9ar1klykjr6m1";
};
buildInputs = [
pypkgs.numpy
nixpkgs.pkgs.gcc
nixpkgs.pkgs.gfortran
];
doCheck = false;
};
- Build derivations for Python packages
- Fetch from PyPI
- Specify build time, run time, test time and native dependencies
- Either runs "setup.py install" or "setup.py develop"
Pypi2NIX
$ pypi2nix -V "3.6" -r requirements.txt
$ nix-shell --pure requirements.nix
- Fast Nix expression generation
- Complicated to debug
- Hardwires Python version
- New Nixpkgs (19.03) now installs missing Python dependencies into
${tmp_path}/lib/pythonX.Y/site-packages
Mixed Language EnvironmentS
$ nix-shell --pure
[nix-shell]$ python --version
Python 3.6.4
[nix-shell]$ node --version
v6.13.0
[nix-shell]$ jekyll serve
Configuration file: /home/wd15/git/pfhub/_config.yml
...
- Maintain web app that uses Python, Ruby (Jekyll) and Node packages
- Share with other developers
- Deploy on CI with exact same build
- Extremely difficult to maintain without Nix
Temporary approach (more pure)
Pip Cheats
$ nix-shell --pure my_python_expression.nix
...
[nix-shell]$ echo $tmp_path
/run/user/33396/tmp.XY6m7ClaxE
[nix-shell]$ pip install --install-option="--prefix=$tmp_path" toolz
/nix/store/sh9x58f3gbs...
...
Successfully installed toolz
Permanent approach (less pure)
postShellHook = ''
SOURCE_DATE_EPOCH=$(date +%s)
export PYTHONUSERBASE=$PWD/.local
export USER_SITE=`python -c "import site; print(site.USER_SITE)"`
export PYTHONPATH=$PYTHONPATH:$USER_SITE
export PATH=$PATH:$PYTHONUSERBASE/bin
'';
$ nix-shell --pure my_python_expression.nix
...
[nix-shell]$ pip install --user toolz
/nix/store/sh9x58f3gbs...
...
Successfully installed toolz
store in .local
buildInputs = [pypkgs.pip, ...
Nix on Travis CI
language: nix
env:
global:
- STORE=$HOME/nix-store
cache:
directories:
- $STORE
before_install:
- sudo mkdir -p /etc/nix
- echo "binary-caches = https://cache.nixos.org/ file://$STORE" | sudo tee -a /etc/nix/nix.conf > /dev/null
- echo 'require-sigs = false' | sudo tee -a /etc/nix/nix.conf > /dev/null
before_cache:
- mkdir -p $STORE
- nix copy --to file://$STORE -f default.nix buildInputs
install:
- nix-shell --pure --command "echo 'run nix-shell once for install'"
script:
- nix-shell --pure --command "py.test"
- Create a local store for cached builds
- Copy the build to the local store for caching
- Use the same build for development and testing
How to fix Nix
- Can't move /nix/store
- Disk space can be large
- Steep learning curve
- Poor documentation
- Can't install Python imperatively
- Extreme pressure on Nixpkgs from all of PyPi, Node, Caball etc.
Nix Frustrations
Improve nix for Python users
- Make nix-env work for Python
- allow imperative installation of Python packages
- Maintain multiple versions of major packages in Nixpkgs
- Something like callHackage for Python to pull and build directly from PyPI
pandoc = nixpkgs.haskellPackages.callHackage "pandoc" "2.4" {};
- Need a declarative standard for Python instead of setup.py, setup.cfg, requirements.txt, MANIFEST.in etc.
- Community of users
Happy to help with Nix
Thanks!
overridePythonAttrs
pypkgs.scipy.overridePythonAttrs rec {
version = "0.19.1";
src = pypkgs.fetchPypi {
pname = "scipy";
inherit version;
sha256 = "1rl411bvla6q7qfdb47fpdnyjhfgzl6smpha33n9ar1klykjr6m1";
};
};
- Override existing nixpkgs recipes
- Use overridePythonAttrs to override package attributes
-
Use override to override package arguments
nix-repl> f = x: x * x # functions
nix-repl> f 2
4
nix-repl> g = x: y: x / y
nix-repl> h = g 9 # currying
nix-repl> h 3
3
nix-repl> { a = 1 / 0; b = 2; }.b
2
nix-repl> { a = 1 / 0; b = 2; }.a # lazy
error: division by zero, at (string):1:7
PURE
Using Nix for Repeatable Python Environments
By Daniel Wheeler
Using Nix for Repeatable Python Environments
- 1,041