Nix Demo

Daniel Wheeler

08/22/2018

Dependency Hell

Existing package and system configuration management tools suffer from an imperative model, where system administration actions such as package upgrades or changes to system configuration files are stateful: they destructively update the state of the system.

 

Actions are not reproducible - they depend on the state of the original system

 

 

 

 

Nix* (not *nix)

  • Nix (package manager)
  • Nix (language)
  • NixOS (linux distro)
  • Nixpkgs (collection of Nix package recipes)
  • NixOps (tools for deploying Nix and NixOS)
  • Hydra CI system
  • Guix (GNU form of Nix based on Scheme)

Timeline

  • Started in 2003
  • NixOS usable in 2007
  • NixOps usable in 2013
  • NixCon 2015, 2017, 2018
  • Nixpkgs on GitHub since 2011
    • 150,126 commits
    • 34,559 closed pull requests
    • 7,671 closed issues
    • 1,761 contributors

The current state of things

  • Traditional package managers are built upon a mutable model
    • e.g. /usr/local is mutable and changes cannot be undone

  • Upgrades are dangerous
  • Rollbacks are only supported via snapshots
  • Testing in different configurations is extremely difficult

Package A

Package B

Package C

Package D

The Dreaded Diamond

Why Nix?

 

 

  • Functional package manager
  • Functional programming language for packaging
  • /nix/store is immutable

  • Actions are independent of state
  • Rollback to any state
  • Multiple versions side-by-side
  • Deterministic & reproducible builds

Functional Programming

  • Mathematical definition of a function -- not a subroutine, but a mathematical expression
  • Functions do not have side effects (pure)
  • Referential transparency -- evaluate to same result in any context
f \left( \;\;\; \right)
# Nix Language Demo

We are going to demo the Nix language using the REPL. Fire up the nix-repl for interactive Nix commands.

    $ nix repl

Some basic syntax and types.

    > 1 + 2
    3
    > true || false
    true
    > if 3 < 4 then "aa" else "bb"
    "a"

There is no floating-point type in Nix.

    > builtins.typeOf 1
    "int"
    > builtins.typeOf { a = 1; }
    "set"
    > { a = 1; b = 2; }.b
    2

Nix has recursive sets.

    > rec { a = 2; b = a; }

Nix has a primitive type for paths.

    > ./hello_world.txt
    /home/wd15/git/nix-presentation/hello_world.txt
    > builtins.typeOf /bin/which
    "path"

Nix is dynamically typed not statically typed (like Haskell). Nix is
strongly typed (not weakly typed like Javascript).

However, Nix is a purely functional language.

    > f = x: x * x
    > f 2
    4
    > (x: y: x + y) 2 3
    5

Functions are first class citizens and you get currying for free. Nix functions only take one argument.

    > g = x: y: x / y
    > h = g 9
    > h 3
    3

Nix functions are automatically curried (like in Haskell).

    > /9
    /9

is a path

Put nix expression in a file

    $ more add.nix
    3 + 1
    > a = import ./add.nix
    > a
    4

Nix is lazy.

    > { a = 1 / 0; }
    { a = error: division by zero, at (string):1:6
    > { a = 1 / 0; b = 2; }.b
    2

Nix is pure and lazy. No loops, but uses recursion for looping. We
don't have time to get into this now.

## Impurity in Nix.

Read from a file

    > data = builtins.readFile ./hello_world.txt
    > data
    "Hello World!\n"

Write to a file

    > builtins.toFile "foo.txt" data
    "/nix/store/mm4fq57cw5gkjgn1c2yysgrq6cd1w6z2-foo.txt"

You can't write to anywhere other than the Nix Store where state
cannot be overwritten.

    > builtins.toFile "foo.txt" (data + "blah")
    "/nix/store/la07gyzhm57f1wxz7v3ypb4gyhnaglaj-foo.txt"

It writes it to a different file.
# Nix at the command line

Start off by making a new profile

    $ nix-env -S $NIX_USER_PROFILE_DIR/demo

What does this do?

    $ ls -altr ~/.nix-profile

This is the equivalent of a new environment with Conda.

    $ nix-env --list-generations

We have no packages installed in this environment. No iterations. Every time we install
something with Nix we get a new iteration of a profile.

    $ nix-env -i hello
    installing ‘hello-2.10’
    ...
    $ hello
    Hello, world!
    $ echo $PATH

How to find the path of what we've installed. Nix uses symlinks to segregate packages, profiles and environments.

    $ which hello
    $ find `which hello` -exec realpath {} +
    $ ls -al /home/wd15/.nix-profile
    $ ls -al /home/wd15/.nix-profile/
    $ ls -al /home/wd15/.nix-profile/bin

To check what we have installed.

    $ nix-env -q
    hello-2.10

To check the generations.

    $ nix-env --list-generations
    1   2018-08-02 17:23:38   (current)

Let's install something else

    $ nix-env -i python
    $ nix-env --list-generations
       1   2018-08-15 10:46:38
       2   2018-08-15 10:47:22   (current)
    $ nix-env -q
    $ which python
    $ ls -al /home/wd15/.nix-profile/bin

Every time we do a new action we get a new environment. Let's install numpy

    $ nix-env -qaP | grep numpy
    ...
    $ nix-env -i python2.7-numpy-1.14.1
    installing ‘python2.7-numpy-1.14.1’
    ...
    $ nix-env -q
    hello-2.10
    python2.7
    python2.7-numpy-1.14.3
    $ nix-env --list-generations
       1   2018-08-02 17:23:38
       2   2018-08-02 17:39:28   (current)
       3   ...
    $ python -c "import numpy"


We have Python but we can't import numpy. Numpy is only installed as a
library. It is not in the Python site-packages directory. Nix installs
Numpy as a regular library not a Python library so it is not accesible
from Python. To use both Numpy, we need to use `nix-shell`.

    $ nix-shell --pure -p python27 -p python27Packages.numpy

Try using it

    $ python -c "import numpy"
    '1.14.3'

the links are set up using `PYTHONPATH`

    $ echo $PYTHONPATH
    $ exit

Let's install something else.

    $ nix-env -i git
    $ nix-env --list-generations
       1   2018-08-02 17:23:38
       2   2018-08-02 17:39:28
       3   ...
       4   2018-08-06 15:19:52   (current)
    $ nix-env -G 1
    $ nix-env -q
    $ nix-env -G 4
    $ nix-env -q
# Building Packages

We have a small script that we'd like to install.

    $ more my-hello

I've also created a build script

    $ more build.sh

First, load all of the nixpkgs

    > :l <nixpkgs>

Then we create something called a "derivation". A "derivation" is a method to generate a new item in the store from other items already in the store.

    > d = builtins.derivation { name = "my-hello"; builder = "${bash}/bin/bash"; args = [ ./build.sh ]; src = ./my-hello; system = builtins.currentSystem; coreutils = coreutils; blah = ./blah; }

    > d
    «derivation /nix/store/fa1vwhhlpwjw2hb4lmv7mz8800w9dk52-my-hello.drv»

    > :b d

All the variables that are passed into the build.sh are only available
from Nix.

    $ cat /nix/store/fa1vwhhlpwjw2hb4lmv7mz8800w9dk52-my-hello.drv

    $ /nix/store/sbqk5hgacg3vm1p52mkk431k1l2q1v8f-my_hello/bin/my-hello

This expression can be written to a Nix script.

    $ more default.nix

Nix build is basically two steps.

    $ nix-instantiate default.nix
    /nix/store/wvmri7vj7ff0rvn0k5gs7nfcg6f8l2d0-my_hello.drv
    $ # Modify build.sh
    $ nix-instantiate default.nix
    /nix/store/wvmri7vj7ff0rvn0k5gs7nfcg6f8l2d0-my_hello.drv
    $ nix-store --realize /nix/store/wvmri7vj7ff0rvn0k5gs7nfcg6f8l2d0-my_hello.drv

This can also be installed in the current environment.

    $ nix-env -if default.nix
    $ my-hello

Dependency Graph

nix-store -q --graph $(nix-store --realise /nix/store/...-python3.6-pandas-0.22.0.drv) | dot -Tpng

Python Builds

  • pypi2nix
$ pypi2nix -V "3.6" -r requirements.txt
$ nix-shell --pure requirements.nix
  • buildPythonPackage
{ nixpkgs, pypkgs }:
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
  ];
  ignoreCollisions=true;
  catchConflicts=false;
  doCheck = false;
}

Example: FiPy

{ nixpkgs, pypkgs, pysparse, preshellhook }:
let
  gmsh = import ./gmsh.nix { inherit nixpkgs; };
  skfmm = import ./skfmm.nix { inherit pypkgs; };
in
  pypkgs.buildPythonPackage rec {
     pname = "fipy";
     version = "3.1.3.dev";
     env = nixpkgs.buildEnv { name=pname; paths=buildInputs; };
     buildInputs = [
       pypkgs.pip
       pypkgs.python
       pypkgs.numpy
       pypkgs.scipy
       gmsh
       skfmm
       pysparse
       pypkgs.matplotlib
       pypkgs.tkinter
     ];
     src=./..;
     doCheck=false;
     preShellHook = preshellhook version;
     prePatch = preshellhook version;
     meta = {
       homepage = "https://www.ctcms.nist.gov/fipy/";
       description = "A Finite Volume PDE Solver Using Python";
       version = version;
       license = nixpkgs.stdenv.lib.licenses.free;
     };
     catchConflicts=false;
     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
       #pip install --user package
     '';
  }
{ nixpkgs ? import ./nixpkgs_version.nix }:
let
  pypkgs = nixpkgs.python27Packages;
  pysparse = import ./pysparse.nix { inherit nixpkgs pypkgs; };
  preshellhook = _: '' '';
in
  import ./build.nix { inherit nixpkgs pypkgs pysparse preshellhook; }
{ nixpkgs ? import ./nixpkgs_version.nix }:
let
  pypkgs = nixpkgs.python36Packages;
  pysparse = null;
  preshellhook = version: ''
    2to3 --write . &> /dev/null;
    2to3 --write --doctests_only . &> /dev/null;
    sed -i 's/version = getVersion()/version="${version}"/' setup.py
  '';
in
  import ./build.nix { inherit nixpkgs pypkgs pysparse preshellhook; }

## After running nix-shell
##
## $ find . -type f -name '*.bak' -delete
## $ git checkout .
##
## to return FiPy back to Python 2.

Python 2

Python 3

Example: PFHub

{ nixpkgs }:
let
  pkgs = nixpkgs.pkgs;
  jekyll_env = nixpkgs.bundlerEnv rec {
    name = "jekyll_env";
    ruby = nixpkgs.ruby;
    gemfile = ./Gemfile;
    lockfile = ./Gemfile.lock;
    gemset = ./gemset.nix;
  };
  pypkgs = nixpkgs.python36Packages;
  pypi2nix = import ./requirements.nix { inherit pkgs; };
  nbval = import ./nbval.nix { inherit pkgs; };
  node = import ./node.nix { inherit pkgs; };
in
  [
    jekyll_env
    nbval
    pkgs.python36
    pypkgs.pillow
    pypkgs.numpy
    pypkgs.toolz
    pypkgs.matplotlib
    pypkgs.flake8
    pypkgs.pylint
    pypi2nix.packages."pykwalify"
    pypi2nix.packages."vega"
    pypi2nix.packages."progressbar2"
    python36Packages.pytest
    pkgs.nodejs
    node.mocha
    node.coffeelint
    node."assert"
    node.surge
    node.execjs
  ]

Nix on Travis CI

---
language: nix
env:
  global:
    - STORE=$HOME/nix-store
  matrix:
    - PYMKS_USE_FFTW=0
    - PYMKS_USE_FFTW=1
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-build -A buildInputs
script:
  - nix-shell --pure --command "export PYMKS_USE_FFTW=$PYMKS_USE_FFTW; py.test"
  - nix-shell --pure --command "pylint --rcfile=.pylintrc pymks/fmks pymks/fmks/tests"
  - nix-shell --pure --command "flake8 pymks/fmks pymkgs/fmks/tests"
  - nix-shell --pure --command "black --check pymks/fmks pymks/fmks/tests"

References

Happy to help with Nix

Thanks!

Nix Demo

By Daniel Wheeler

Nix Demo

  • 804