## title: My Nix exploration
## date: "2024-06-24"
/nix_logo.svg
(IMG) /nix_logo.svg
Here I share some notes and other things I've learned about
Nix that I find interesting. The content of this post is
mainly about me learning Nix, it's not about understanding
the whole tool and language.
Also, it's important to note that I use Nix as a non-NixOS
user.
## What is Nix?
Nix is actually several things! It's a cross platform
package manager. It would be a little more accurate to say
that it's a deployment tool used as a package manager.
And it's also a purely functional programming language,
dynamically typed and lazily evaluated.
## Learning the programming language
I started by learning the basics of the language and then
went on to explore it in a bit more depth.
### The basics
I read Nix language basics and to get used to the language I
practised with A tour of Nix which has several levels of
difficulty from "easy" to "hard".
(HTM) Nix language basics
(HTM) A tour of Nix
One interesting thing about this language is that it has
only one argument per function. To simulate several
arguments, you can, for example, write a function with one
argument that returns a function with one argument that
returns a function with one argument, and so on. The syntax
of the language makes it easy to do this.
I was taught that it has a name, it's called Currying. It's
the transformation of a function with several arguments into
a function with one argument that returns a function on the
rest of the arguments. Here's an example with arguments 3
and 4.
(HTM) Currying
nix-repl> (a: b: a + b) 3 4
7
A Python equivalent might be something like the following.
>>> (lambda a: lambda b: a + b)(3)(4)
7
Another solution that is often used, particularly in
Nixpkgs, is to have an attribute set as a parameter to the
function, and to use the attributes as arguments. For
example, this might look like the expression below.
(HTM) Nixpkgs
nix-repl> ({a, b}: a + b){a = 3; b = 4;}
7
### Fake dynamic binding
Although the blog post How to Fake Dynamic Binding in Nix
talks about this very well, I find it interesting to offer
my own thoughts and approach.
(HTM) How to Fake Dynamic Binding in Nix
The language is statically scoped, i.e. binding decisions
are made according to the scope at declaration time.
Let's look at the rec keyword, which allows an attribute set
to access its own attributes (recursive binding). Here's an
example.
nix-repl> rec { a = 1; b = a + 1;}
{
a = 1;
b = 2;
}
This is an interesting feature, but it remains static
because the binding is done before the runtime. This poses
problems, particularly when it comes to overriding
attributes, as shown in the example below.
nix-repl> rec { a = 1; b = a + 1; } // { a = 10; }
{
a = 10;
b = 2;
}
In this example, we would like b to be equal to 11, not 2.
To solve this problem, we can look at the concept of a fixed
point. A fixed point is a value of x that validates the
equation x = f(x).
We can therefore write the following function.
nix-repl> fix = f: let
result = f result;
in
result
So here we have the function fix which takes a function f as
a parameter and returns the fixed point result of the
function f.
You might be tempted to say that the f function calls itself
ad infinitum (f(f(f(f(..))))), but Nix evaluates expressions
lazily, so this isn't the case.
We can literally see that the f function returns a fixed
point (result), because result = f result, which respects
the definition of a fixed point.
The fix function will allow us to emulate the rec keyword,
as shown in the example below.
nix-repl> fix (self: { a = 3; b = 4; c = self.a + self.b; })
{
a = 3;
b = 4;
c = 7;
}
To better understand how it works, I've written the result
of the fix function differently with the argument used
previously.
nix-repl> let
result = { a = 3; b = 4; c = result.a + result.b;};
in
{ a = 3; b = 4; c = result.a + result.b;}
{
a = 3;
b = 4;
c = 7;
}
Finally, I've written the following function, which will
allow the attributes to be overridden dynamically as
initially intended.
nix-repl> fix = let
fixWithOverride = f: overrides: let
result = (f result) // overrides;
in
result // { override = x: fixWithOverride f x; };
in
f: fixWithOverride f {}
attrFunction = self: { a = 3; b = 4; c = self.a+self.b; }
attrFunctionFixedPoint = fix attrFunction
nix-repl> attrFunctionFixedPoint
{
a = 3;
b = 4;
c = 7;
override = «lambda override @ «string»:5:30»;
}
nix-repl> attrFunctionFixedPoint.override { b = 1; }
{
a = 3;
b = 1;
c = 4;
override = «lambda override @ «string»:5:30»;
}
## The essential Nix tool
As already mentioned, the main use of Nix is cross platform
package management. In this section I'm just trying to share
and summarise some of the essential parts of my notes. If
you want more details, I recommend you read the excellent
Nix Pills. It's rather long but well worth the read!
(HTM) Nix Pills
### How does it work ?
To sum up, I'd say that the Nix language has a very
interesting native function called derivation (see
documentation) on which many Nix expressions are based. I'm
not going to redefine the term because the documentation has
a very comprehensible version, but the important thing to
remember is that a derivation is a construction
specification, it's an immutable Nix building block. With
another package manager, you could see it as a literal
package.
(HTM) see documentation
Nix technology will enable us to build these derivations, in
the following stages.
/nix_graph.png
(IMG) /nix_graph.png
The .drv files contain specifications on how to build the
derivation, they are intermediate files comparable to .o
files, and the .nix files are comparable to .c files.
The construction result is immutable and will be stored in
/nix/store/, a synchronisation with the SQLite database. I
said it was immutable, in fact it is because Nix creates a
hash for the path in the /nix/store/ from the input
derivation (not from the construction result).
(HTM) SQLite
It's pretty hard to imagine all this, so I'll give you a
concrete example. Let's imagine I want to create a
derivation for the famous software GNU Hello. The Nix
derivation could look something like this.
(HTM) GNU Hello
# default.nix
let
pkgs = import <nixpkgs> { };
in
{
hello = pkgs.stdenv.mkDerivation {
pname = "hello";
version = "2.12.1";
src = fetchTarball {
url = "https://ftp.gnu.org/gnu/hello/hello-
2.12.1.tar.gz";
sha256 =
"1kJjhtlsAkpNB7f6tZEs+dbKd8z7KoNHyDHEJ0tmhnc=";
};
};
}
“The mkDerivation function is based on the derivation
builtin function.”
It can be built with the following command.
nix-build
The build result has been created in
/nix/store/x9cc4jsylk5q01iaxmxf941b59chws5h-hello-2.12.1 and a
symbolic link named result pointing to this folder has been
created in the current folder. We can then find the binary
in ./result/bin/hello.
Before the build, a .drv file was created, which can be
found by running the following command.
nix derivation show ./result | jq "keys[0]"
The full path to the .drv file is found in the first key of
the JSON object, so the path to the .drv file is
/nix/store/dp5z62k3chf019biikg77p2acmz17phx-hello-2.12.1.drv.
As it is in binary format we can use nix derivation show to
display the construction information it contains with the
following command.
nix derivation show (nix derivation show ./result | jq
"keys[0]" | tr -d "\"")
# Or
nix derivation show
/nix/store/dp5z62k3chf019biikg77p2acmz17phx-hello-2.12.1.drv
# ^
# | Same output
# v
nix derivation show ./result
### Nixpkgs
In the Nix expression used previously (the GNU Hello
derivation), I used the mkDerivation function from stdenv.
(HTM) GNU Hello
This function is not builtin, it comes from the pkgs
identifier which has the value import <nixpkgs> { };.
Before explaining this import, I think it's very important
to understand what Nixpkgs is. It's a Git repository that
contains all the Nix expressions and modules. When this
folder is evaluated, it produces an attribute set containing
stdenv, which is itself an attribute set containing our
mkDerivation function.
(HTM) Nixpkgs
Getting back to pkgs, <nixpkgs> is just a special Nix
syntax, which, when evaluated, gives a path to a folder
containing a collection of Nix expressions, i.e. Nixpkgs.
Incidentally <nixpkgs> has an equivalence in Nix as shown
below.
nix-repl> <nixpkgs>
/home/nagi/.nix-defexpr/channels/nixpkgs
nix-repl> builtins.findFile builtins.nixPath "nixpkgs"
/home/nagi/.nix-defexpr/channels/nixpkg
nix-repl> :p builtins.nixPath
[
{
path = "/home/nagi/.nix-defexpr/channels";
prefix = "";
}
]
### Managing multiple Python versions
One of the advantages of Nix is that it naturally offers the
possibility of managing several versions of the same
application. Taking Python as an example, let's say I want a
Nix shell with version 3.7 and version 3.13.
(HTM) Python
To do this, we can check for which version of Nixpkgs Python
was built on version 3.7 and target a specific version of
Nixpkgs in our Nix expression.
(HTM) Nixpkgs
(HTM) Nixpkgs
To do this, there's the flox tool which works very well, but
to make it easier to understand I prefer to use nixhub.io.
(HTM) flox
(HTM) nixhub.io
So I'm looking for a version of the Nix packages that
corresponds to Python version 3.7, and I find
nixpkgs/aca0bbe791c220f8360bd0dd8e9dce161253b341#python37.
# shell.nix
let
pkgs = import (fetchTarball
"https://github.com/NixOS/nixpkgs/tarball/nixos-23.11") { };
nixpkgs-python = import (fetchTarball
"https://github.com/NixOS/nixpkgs/archive/aca0bbe791c220f8360bd0dd8e9dce161253b341.tar.gz")
{ };
in
pkgs.mkShell {
buildInputs = [
nixpkgs-python.python37
pkgs.python313
];
}
You can build Python derivations and enter a Nix shell with
the following command.
nix-shell
And we see that we have access to the two versions requested
with the commands python3.7 and python3.13 !
## A Virtual environment in Python with Nix flakes
I've recently created a development environment with Nix
flakes (see documentation), it's very handy as it provides a
ready to use environment for Python 3.11 with the desired
modules.
(HTM) see documentation
Below is a Nix expression I wrote for the Python module
callviz, it has all the necessary dependencies and a virtual
Python environment.
(HTM) callviz
# flake.nix
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
};
outputs =
{ self, nixpkgs }:
let
supportedSystems = [
"x86_64-linux"
"aarch64-linux"
"x86_64-darwin"
"aarch64-darwin"
];
forEachSupportedSystem =
f: nixpkgs.lib.genAttrs supportedSystems (system: f
{ pkgs = import nixpkgs { inherit system; }; });
in
{
# ...
# I usually also declare a default package, a code
checker and formatter
devShells = forEachSupportedSystem (
{ pkgs }:
{
default = pkgs.mkShell {
venvDir = ".venv";
packages =
with pkgs;
[
python3
graphviz
]
++ (with pkgs.python3Packages; [
pip
venvShellHook
graphviz
]);
};
}
);
};
}
Note that the default package and the default development
shell are compatible with all systems (supportedSystems)!
To realise the derivations and enter the Nix shell, I can
run the following command.
nix develop
## Nixpkgs contribution
Once I'd finished exploring and learning Nix, I wanted to
make a package for Super Mario War and add it to Nixpkgs.
(HTM) Super Mario War
(HTM) Nixpkgs
Here's what the package looks like.
{
lib,
stdenv,
fetchFromGitHub,
cmake,
pkg-config,
enet,
yaml-cpp,
SDL2,
SDL2_image,
SDL2_mixer,
zlib,
unstableGitUpdater,
makeWrapper,
}:
stdenv.mkDerivation (finalAttrs: {
pname = "supermariowar";
version = "2024-unstable-2025-06-18";
src = fetchFromGitHub {
owner = "mmatyas";
repo = "supermariowar";
rev = "71383b07b99a52b57be79cf371ab718337365019";
hash = "sha256-
PjweE8cGAp8V4LY0/6QzLekQ80Q1qbwDiiSzDirA29s=";
fetchSubmodules = true;
};
nativeBuildInputs = [
cmake
pkg-config
makeWrapper
];
buildInputs = [
enet
yaml-cpp
SDL2
SDL2_image
SDL2_mixer
zlib
];
cmakeFlags = [ "-DBUILD_STATIC_LIBS=OFF" ];
postInstall = ''
mkdir -p $out/bin
for app in smw smw-leveledit smw-worldedit; do
makeWrapper $out/games/$app $out/bin/$app
--add-flags "--datadir $out/share/games/smw"
done
ln -s $out/games/smw-server $out/bin/smw-server
'';
passthru.updateScript = unstableGitUpdater { };
meta = {
description = "Fan-made multiplayer Super Mario Bros.
style deathmatch game";
homepage = "https://github.com/mmatyas/supermariowar";
changelog =
"https://github.com/mmatyas/supermariowar/blob/${finalAttrs.src.rev}/CHANGELOG";
license = lib.licenses.gpl2Plus;
maintainers = with lib.maintainers; [ theobori ];
mainProgram = "smw";
platforms = lib.platforms.linux;
};
})