My Nix exploration | |
2024-06-24 | |
Last edit: 2024-06-24 | |
--------------------- | |
Here I share some notes and other things I've learned about Nix that I find int… | |
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… | |
And it's also a purely functional programming language, dynamically typed and l… | |
## Learning the programming language | |
I started by learning the basics of the language and then went on to explore it… | |
### The basics | |
I read | |
Nix language basics](https://nix.dev/tutorials/nix-language#reading-nix-languag… | |
which has several levels of difficulty from "easy" to "hard". | |
One interesting thing about this language is that it has only one argument per … | |
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 … | |
```nix | |
nix-repl> (a: b: a + b) 3 4 | |
7 | |
``` | |
A Python equivalent might be something like the following. | |
```python | |
>>> (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 at… | |
```nix | |
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… | |
The language is statically scoped, i.e. binding decisions are made according to… | |
Let's look at the `rec` keyword, which allows an attribute set to access its ow… | |
```nix | |
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 do… | |
```nix | |
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 poi… | |
We can therefore write the following function. | |
```nix | |
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 an… | |
You might be tempted to say that the `f` function calls itself ad infinitum (`f… | |
We can literally see that the `f` function returns a fixed point (`result`), be… | |
The `fix` function will allow us to emulate the `rec` keyword, as shown in the … | |
```nix | |
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` functio… | |
```nix | |
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 t… | |
```nix | |
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.… | |
Nix Pills | |
. It's rather long but well worth the read! | |
### How does it work ? | |
To sum up, I'd say that the Nix language has a very interesting native function… | |
see documentation | |
) on which many Nix expressions are based. I'm not going to redefine the term b… | |
Nix technology will enable us to build these derivations, in the following stag… | |
The `.drv` files contain specifications on how to build the derivation, they ar… | |
The construction result is immutable and will be stored in `/nix/store/`, a syn… | |
SQLite | |
database. I said it was immutable, in fact it is because Nix creates a hash fo… | |
It's pretty hard to imagine all this, so I'll give you a concrete example. Let'… | |
GNU Hello | |
. The Nix derivation could look something like this. | |
```nix | |
# default.nix | |
let | |
pkgs = import { }; | |
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. | |
```bash | |
nix-build | |
``` | |
The build result has been created in `/nix/store/x9cc4jsylk5q01iaxmxf941b59chws… | |
Before the build, a `.drv` file was created, which can be found by running the … | |
```bash | |
nix derivation show ./result | jq "keys[0]" | |
``` | |
The full path to the `.drv` file is found in the first key of the JSON object, … | |
As it is in binary format we can use `nix derivation show` to display the const… | |
```bash | |
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`. | |
This function is not builtin, it comes from the `pkgs` identifier which has the… | |
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. W… | |
Getting back to `pkgs`, `` is just a special Nix syntax, which, when evaluated,… | |
Incidentally `` has an equivalence in Nix as shown below. | |
```nix | |
nix-repl> | |
/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 man… | |
Python | |
as an example, let's say I want a Nix shell with version 3.7 and version 3.13. | |
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. | |
To do this, there's the | |
flox](https://floxdev.com/) tool which works very well, but to make it easier t… | |
. | |
So I'm looking for a version of the Nix packages that corresponds to Python ver… | |
```nix | |
# shell.nix | |
let | |
pkgs = import (fetchTarball "https://github.com/NixOS/nixpkgs/tarball/nixos-2… | |
nixpkgs-python = import (fetchTarball "https://github.com/NixOS/nixpkgs/archi… | |
in | |
pkgs.mkShell { | |
buildInputs = [ | |
nixpkgs-python.python37 | |
pkgs.python313 | |
]; | |
} | |
``` | |
You can build Python derivations and enter a Nix shell with the following comma… | |
```bash | |
nix-shell | |
``` | |
And we see that we have access to the two versions requested with the commands … | |
## 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 wi… | |
Below is a Nix expression I wrote for the Python module | |
callviz | |
, it has all the necessary dependencies and a virtual Python environment. | |
```nix | |
# 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 nix… | |
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 … | |
To realise the derivations and enter the Nix shell, I can run the following com… | |
```bash | |
nix develop | |
``` | |
## Nixpkgs contribution | |
Once I'd finished exploring and learning Nix, I wanted to make a package for [S… | |
Nixpkgs | |
. | |
Here's what the package looks like. | |
```nix | |
{ | |
lib, | |
stdenv, | |
fetchFromGitHub, | |
cmake, | |
pkg-config, | |
enet, | |
yaml-cpp, | |
SDL2, | |
SDL2_image, | |
SDL2_mixer, | |
zlib, | |
unstableGitUpdater, | |
makeWrapper, | |
}: | |
stdenv.mkDerivation (finalAttrs: { | |
pname = "supermariowar"; | |
version = "2023-unstable-2024-09-17"; | |
src = fetchFromGitHub { | |
owner = "mmatyas"; | |
repo = "supermariowar"; | |
rev = "6b8ff8c669ca31a116754d23b6ff65e42ac50733"; | |
hash = "sha256-P0jV7G81thj0UJoYLd5+H5SjjaVu4goJxc9IkbzxJgs="; | |
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 = "A fan-made multiplayer Super Mario Bros. style deathmatch ga… | |
homepage = "https://github.com/mmatyas/supermariowar"; | |
changelog = "https://github.com/mmatyas/supermariowar/blob/${finalAttrs.src… | |
license = lib.licenses.gpl2Plus; | |
maintainers = with lib.maintainers; [ theobori ]; | |
mainProgram = "smw"; | |
platforms = lib.platforms.linux; | |
}; | |
}) | |
``` | |