Partie 6 : Vers une normalisation des dérivations
Bonjour à toutes et tous! 😀
Sixième article sur Nix.
Les dérivations pures c'est bien, mais elles donnent un peu trop de liberté.
Absence de standard
Le problème de l'informatique est souvent l'interopérérabilité.
L'interopérabilté est le fait de permettre à deux sytèmes de pouvoir se parler au travers d'un canal normalisé.
Prenons la dérivation de la partie 4 :
with import (fetchTarball {
url = "https://github.com/NixOS/nixpkgs/archive/release-21.11.tar.gz";
sha256 = "1xk1f62n00z7q5i3pf4c8c4rlv5k4jwpgh0pqgzw1l40vhdkixk9";
}) {};
derivation {
name = "hello-world";
system = "x86_64-linux";
builder = "pkgs.bash/bin/bash";
args = [
"-c"
''
gcc $SOURCE -o $out
''
];
PATH = "gcc/bin";
SOURCE = builtins.toFile "main.c" ''
#include "stdio.h"
void main() {
printf("Hello World\n");
}
'';
}
Elle complile les sources vers le chemin $out
.
Et donc le $out
est un exécutable.
A l'inverse, le paquet ${pkgs.bash}
a son binaire dans le dossier $out/bin/bash
.
Pour coller à ce mode fonctionnement, nous allons devoir modifier quelques peu notre dérivation.
La première est de créer le dossier $out/bin
que l'on souhaite devenir la destination du produit de compilation.
On modifie également le paramètre -o $out
de gcc en -o $out/bin/hello-world
.
On rajoute aussi coreutils
dans le PATH pour accéder à la commande mkdir
.
nix-shell> with import (fetchTarball {
url = "https://github.com/NixOS/nixpkgs/archive/release-21.11.tar.gz";
sha256 = "1xk1f62n00z7q5i3pf4c8c4rlv5k4jwpgh0pqgzw1l40vhdkixk9";
}) {};
derivation {
name = "hello-world";
system = "x86_64-linux";
builder = "${pkgs.bash}/bin/bash";
args = [
"-c"
''
mkdir -p $out/bin
gcc $SOURCE -o $out/bin/hello-world
''
];
PATH = "${gcc}/bin:${coreutils}/bin/";
SOURCE = builtins.toFile "main.c" ''
#include "stdio.h"
void main() {
printf("Hello World\n");
}
'';
}
This derivation produced the following outputs:
out -> /nix/store/4wvz6l7l436ma226k4gky9985fanv8cg-hello-world
On peut ainsi visualiser le contenu de la réalisation de la dérivation.
On se retrouve bien avec une structure semblable à celle de ${pkgs.bash}/bin/bash
.
Inputs
L'intérêt principal de normaliser des manières de faire c'est que l'on peut facilement automatiser.
Par exemple, la définition du PATH.
Si tout les paquets respectent le même schéma de définitions des binaires.
PATH = "{bash}/bin:{gcc}/bin:{coreutils}/bin".
Alors, on peut simplifier l'écriture pour passer par un tableau.
buildInputs = [ bash gcc coreutils ]
Bon très bien, mais il n'y aura pas grand chose qui se passera.
$buildInputs
n'est qu'une variable d'environnement.
Nous allons devoir la faire vivre.
Setup
Nous allons établir une stratégie permettant d'utiliser notre variable $buildInputs
.
Pour cela, nous allons déterminer un script qui fera le travail à notre place.
Je le défini délibérément naïf, dans la réalité, il faudrait le rendre plus versatile, mais ça suffira pour la démo.
On reset d'abord le $PATH
.
Puis on profite de la propriété de la dérivation qui lorsque que l'on l'interpole sous la forme d'une chaîne devient le $out
de la dérivation.
Propriété qui fonctionne également avec un tableau.
nix-repl> toString [ bash gcc coreutils ]
"/nix/store/zlf0f88vj30sc7567b80l52d19pbdmy2-bash-5.2-p15
/nix/store/nlgyw2fv0cm8rkz8qm1jyw78vyif1bl9-gcc-wrapper-12.2.0
/nix/store/arbxkmcgv9h8pjgj95c6d7r86yb77rl5-coreutils-9.1"
Comme nous avons choisi que les binaires serait dans le dossier $out/bin
. Nous pouvons créer notre $PATH
automatiquement.
PATH=""
for; do
PATH=" : /bin"
done
Nous allons alors faire deux choses:
- définir un fichier
setup.sh
via la même méthode que pour les sources - utiliser la
source
qui permet d'exécuter le bash contenu dans un fichier
La réunion de ces deux actions nous donnes la dérivation suivante:
with import (fetchTarball {
url = "https://github.com/NixOS/nixpkgs/archive/release-21.11.tar.gz";
sha256 = "1xk1f62n00z7q5i3pf4c8c4rlv5k4jwpgh0pqgzw1l40vhdkixk9";
}) {};
derivation {
name = "hello-world";
system = "x86_64-linux";
builder = "pkgs.bash/bin/bash";
args = [
"-c"
''
source $SETUP
# builder
mkdir -p $out/bin
gcc $SOURCE -o $out/bin/hello-world
''
];
SETUP = builtins.toFile "setup.sh" ''
PATH=""
for input in $buildInputs; do
PATH="$PATH:$input/bin"
done
'';
buildInputs = [ bash gcc coreutils ];
SOURCE = builtins.toFile "main.c" ''
#include "stdio.h"
void main() {
printf("Hello World\n");
}
'';
}
Cela peut sembler étrange de vouloir écrire plus de lignes pour le même résultat, mais nous sommes en train de nous rapprocher de l'automatisation.
Et nous allons aller encore plus loin !
Builder
Nous allons sortir dans un builder.sh
le contenu du build.
Je vais alors définir deux variables:
$setupPhase
anciennement$SETUP
$buildPhase
le chemin du builder.
Et on rajoute le source builder.sh
qui va bien.
with import (fetchTarball {
url = "https://github.com/NixOS/nixpkgs/archive/release-21.11.tar.gz";
sha256 = "1xk1f62n00z7q5i3pf4c8c4rlv5k4jwpgh0pqgzw1l40vhdkixk9";
}) {};
derivation {
name = "hello-world";
system = "x86_64-linux";
builder = "pkgs.bash/bin/bash";
args = [
"-c"
''
source $setupPhase
source $buildPhase
''
];
buildPhase = builtins.toFile "builder.sh" ''
mkdir -p $out/bin
gcc $SOURCE -o $out/bin/hello-world
'';
setupPhase = builtins.toFile "setup.sh" ''
PATH=""
for input in $buildInputs; do
PATH="$PATH:$input/bin"
done
'';
buildInputs = [ bash gcc coreutils ];
SOURCE = builtins.toFile "main.c" ''
#include "stdio.h"
void main() {
printf("Hello World\n");
}
'';
}
Ok, et donc, ça nous avance à quoi ?
J'y arrive ^^ ^
Normalisation de la dérivation
Maintenant que nous avons normalisé ce que l'on pouvais, nous allons pouvoir passer à l'automatisation !
Pour cela, nous allons créer une fonction qui va retourner une dérivation.
Mais avant de pouvoir y arriver, nous allons encore manipuler un peu de Nix.
Ne vous plaignez pas c'est pour votre bien. ^^ C'est comme pour aller à l'étranger, savoir commander une bière c'est bien.
Pouvoir également dire en cas d'arrestation: "Je suis français, je voudrai appeler l'Ambassade" dans la langue, c'est mieux ^^
Nous allons donc découvrir d'autres syntaxes de la grammaire.
Paramètres optionels
Lorsque l'on défini une fonction en Nix, nous pouvons lui donner un set en entrée, ce set possède des champs qui sont nommés.
Si notre fonction a pour signature
f = {x, y} : x + y
Il est obligatoire de remplir tous les champs
nix-repl> f {x = 12;}
error: function 'anonymous lambda' called without required argument 'y'
Ici, Nix, nous averti qu'il manque un paramètre.
Bien que cela soit pratique dans certains cas, cela peut être ennuyeux. Car l'obligation de conformité est également contraignante lorsqu'il y a trop de paramètres.
nix-repl> f { x = 12; y = 4; z = 42;}
error: function 'anonymous lambda' called with unexpected argument 'z'
Nix attendait seulement un paramètre x et y. Nous lui avons donné un paramètres 'z'.
Sauf que parfois, nous ne contrôlons pas avec exactitude les paramètres d'entrées, il peut y avoir les bons champs au bon nom mais avec des des champs supplémentaires.
data = { x = 12; y = 4; z = 42; t = { a = "chat"; b = true; }; }
A l'appel, nous avons sans surprise une erreur.
nix-repl> f data
error: function 'anonymous lambda' called with unexpected argument 'z'
Il est possible de déclarer différement notre fonction pour lui faire accepter des paramètres optionels.
f2 = {x, y, ...} : x + y
Et maintenant ça marche correctement ^^
nix-repl> f2 data
16
Bien entendu, les règles de champs obligatoires restent valides.
nix-repl> f { x = 12; }
error: function 'anonymous lambda' called without required argument 'y'
Et il est également possible de mélanger les concepts de champs par défaut et optionnel.
f3 = { x, y ? 0, ...} : x + y
Et désormais y n'est plus obligatoire.
nix-repl> f3 { x = 12; }
12
Le mot-clef inherit
Il s'agit d'un sucre syntaxique qui permet de ne pas répéter le champ et la valeur lorsque les deux possèdent le même nom.
nix-repl> x = 12
nix-repl> { x = x; }
{ x = 12; }
Est équivalent à
nix-repl> { inherit x; }
{ x = 12; }
Set Binding
J'ai pas trouvé de traduction satisfaisante.
C'est l'idée que l'on puisse récupérer le set dans une variable.
La syntaxe est:
args @ { x = 12; y = true; }
ou
{ x = 12; y = true; } @ args
On peut utiliser cette propriété pour copier les champs dans un set:
wrapping = inner @ {x, y ? 0, ... } : name :
{ inherit inner; inherit name; }
On peut alors utiliser notre data
et lui donner un label.
nix-repl> wrapping data "toto"
{ inner = { ... }; name = "toto"; }
Fusion (merge) de sets
Il est possible de combiner plusieurs sets en un seul.
nix-repl> { x = 12; } // { y = true; }
{ x = 12; y = true; }
Ce qui peut se faire à répétitions
nix-repl> { x = 12; } // { y = true; } // { z = { a = 12.2; }; }
{ x = 12; y = true; z = { ... }; }
Le merge peut également réécrire des champs.
nix-repl> { x = 12; y = true; } // { x = 42; }
{ x = 42; y = true; }
Supprimer un champ
Ce n'est pas forcément un outil du langage, mais c'est très pratique pour nore besoin.
Nix est fourni avec une série de fonctions, dont une qui nous intéresse builtins.removeAttrs
.
Celle-ci prend deux paramètres:
- le set à modifier
- une liste de noms de champs à supprimer
Comme son nom l'indique permet de supprimer des champs d'un set.
nix-repl> builtins.removeAttrs { x = 12; y = true; z = 12.2; } [ "x" "z" ]
{ y = true; }
Notre fonction makeDerivation
Nous avons tous les outils, maintenant assemblage !!
Que voulons-nous faire déjà ?
Nous avons créé une dérivation normalisée:
derivation {
name = "hello-world";
system = "x86_64-linux";
builder = "pkgs.bash/bin/bash";
args = [
"-c"
''
source $setupPhase
source $buildPhase
''
];
buildPhase = ...;
setupPhase = ...;
buildInputs = ...;
}
Le but est de pouvoir remplir seulement les champs pertinents, nous donnerons le nom de makeDerivation
à notre fonction.
Par exemple, notre setupPhase
sera toujours identique, de même que le builder
.
A contrario, d'autres champs sont à redéfinir pour chaque dérivation, comme buildPhase
, buildInputs
et name
.
On désire également définir le <nixpkgs>
que l'on désire pour conserver la reproductivité.
Et dernière contrainte, nous voulons pouvoir définir n'importe quelle variable d'environnement.
Prenons les choses dans l'ordre, réglons les problèmes un par un.
D'abord les champs obligatoires:
- name
- pkgs
- buildPhase
Et un champ possédant une valeur par défaut buildInputs
.
Ce qui nous donne la signature
makeDerivation = { pkgs, name, buildPhase, buildInputs ? [] } : {}
Ok, rajoutons le corps de la dérivation avec les champs obligatoires:
makeDerivation = { pkgs, name, buildPhase, buildInputs ? [] } :
derivation {
system = "x86_64-linux";
setupPhase = builtins.toFile "setup.sh" ''
PATH=""
for input in $buildInputs; do
PATH="$PATH:$input/bin"
done
'';
builder = "pkgs.bash/bin/bash";
args = [
"-c"
''
source $setupPhase
source $buildPhase
''
];
}
Il manque les champs définis par la fonction, on les rajoute:
makeDerivation = { pkgs, name, buildPhase, buildInputs ? [] } :
derivation {
# champs hérités
inherit name;
inherit buildPhase;
inherit buildInputs;
system = "x86_64-linux";
setupPhase = builtins.toFile "setup.sh" ''
PATH=""
for input in $buildInputs; do
PATH="$PATH:$input/bin"
done
'';
builder = "pkgs.bash/bin/bash";
args = [
"-c"
''
source $setupPhase
source $buildPhase
''
];
}
Sauf que l'on peut être plus malin et utiliser le merge à la place
makeDerivation = { pkgs, name, buildPhase, buildInputs ? [] } @ args :
derivation {
system = "x86_64-linux";
setupPhase = builtins.toFile "setup.sh" ''
PATH=""
for input in $buildInputs; do
PATH="$PATH:$input/bin"
done
'';
builder = "pkgs.bash/bin/bash";
args = [
"-c"
''
source $setupPhase
source $buildPhase
''
];
} // args
Il nous manque un bout, comment rajouter les variables d'environnement personnalisées ?
Nous utilisons les paramètres optionels, plus le merge
makeDerivation = { pkgs, name, buildPhase, buildInputs ? [], ... } @ args :
derivation ( {
system = "x86_64-linux";
setupPhase = builtins.toFile "setup.sh" ''
PATH=""
for input in $buildInputs; do
PATH="$PATH:$input/bin"
done
'';
builder = "pkgs.bash/bin/bash";
args = [
"-c"
''
source $setupPhase
source $buildPhase
''
];
} // args )
Attention!
Il faut bien entouré de paranthèses sinon le merge est appliqué sur le résultat de l'appel à la dérivation et non comme paramètre de dérivation
Ok, c'est pas mal.
Testons notre fonction:
with import (fetchTarball {
url = "https://github.com/NixOS/nixpkgs/archive/release-21.11.tar.gz";
sha256 = "1xk1f62n00z7q5i3pf4c8c4rlv5k4jwpgh0pqgzw1l40vhdkixk9";
}) {};
makeDerivation {
name = "hello-world";
buildPhase = builtins.toFile "builder.sh" ''
mkdir -p $out/bin
gcc $SOURCE -o $out/bin/hello-world
'';
buildInputs = [ bash gcc coreutils ];
inherit pkgs;
SOURCE = builtins.toFile "main.c" ''
#include "stdio.h"
void main() {
printf("Hello World\n");
}
'';
}
Hum, pas fameux ...
… while evaluating derivation 'hello-world'
whose name attribute is located at «string»:7:3
… while evaluating attribute 'pkgs' of derivation 'hello-world'
at «string»:16:10:
15|
16| inherit pkgs;
| ^
17|
Après, je ne vous ai pas parlé du removeAttrs
pour rien 😁
Avant de l'utiliser, qu'est ce qui se passe ?
La méthode dérivation, ne prend pas de paramètre autre que tu type "chaîne de caractères".
Or pkgs
est un set qui ne peut pas être converti en string
, ce pkgs
étant passé lors du merge, il se retrouve dans le paramètre de la dérivation qui le refuse.
On va donc nettoyer notre set d'entré pour qu'il n'y ait plus de champ pkgs
.
makeDerivation =
{ pkgs, name, buildPhase, buildInputs ? [], ... } @args : derivation ({
system = "x86_64-linux";
setupPhase = builtins.toFile "setup.sh" ''
PATH=""
for input in $buildInputs; do
PATH="$PATH:$input/bin"
done
'';
builder = "pkgs.bash/bin/bash";
args = [
"-c"
''
source $setupPhase
source $buildPhase
''
];
} // builtins.removeAttrs args ["pkgs"] )
Nous pouvons alors utiliser notre makeDerivation
fraîchement réparée
deriv = with import (fetchTarball {
url = "https://github.com/NixOS/nixpkgs/archive/release-21.11.tar.gz";
sha256 = "1xk1f62n00z7q5i3pf4c8c4rlv5k4jwpgh0pqgzw1l40vhdkixk9";
}) {};
makeDerivation {
name = "hello-world";
buildPhase = builtins.toFile "builder.sh" ''
mkdir -p $out/bin
gcc $SOURCE -o $out/bin/hello-world
'';
buildInputs = [ bash gcc coreutils ];
inherit pkgs;
SOURCE = builtins.toFile "main.c" ''
#include "stdio.h"
void main() {
printf("Hello World\n");
}
'';
}
Ce qui donne
nix-repl> :b deriv
This derivation produced the following outputs:
out -> /nix/store/ij9lwz1wm1x02zhcix5fsdzxqf969s96-hello-world
$ /nix/store/ij9lwz1wm1x02zhcix5fsdzxqf969s96-hello-world/bin/hello-world
Hello World
C'est plus court et plus lisible, non ? 😀
Essayons avec quelque chose de plus consistant.
Nous allons reprendre le projet github au commit.
On oublie pas les bonnes habitudes et on prefetch pour récupérer la signature:
$ nix-prefetch-url --unpack https://github.com/Akanoa/nix-hello/archive/9e363d35a44b00b190a5fa8376dc2d4a221d94a2.tar.gz
path is '/nix/store/dl6rj73hw5qpj14sk7wazzh16cvrjpvv-9e363d35a44b00b190a5fa8376dc2d4a221d94a2.tar.gz'
106ra1nd19y57rnzndb87d59x1vhr6magy4y46vk98d5x6fhmr6y
Il est construit via Autotools.
Et donc notre buildPhase
sera:
Nous avons besoin de make et de gcc.
On rajoute également une busybox car make demande tout un tas de commandes. Sans busybox
, cela ne fonctionnerait pas. Essayez sans pour voir. 🙂
Bon, nous avons tout, nous pouvons construire notre dérivation
with import (fetchTarball {
url = "https://github.com/NixOS/nixpkgs/archive/release-21.11.tar.gz";
sha256 = "1xk1f62n00z7q5i3pf4c8c4rlv5k4jwpgh0pqgzw1l40vhdkixk9";
}) {};
makeDerivation {
name = "hello-world";
buildPhase = builtins.toFile "builder.sh" ''
cd $SOURCE
./configure --prefix $out
make
make test
make install
'';
buildInputs = [ busybox gcc gnumake ];
inherit pkgs;
SOURCE = fetchTarball {
url = "https://github.com/Akanoa/nix-hello/archive/9e363d35a44b00b190a5fa8376dc2d4a221d94a2.tar.gz";
sha256 = "106ra1nd19y57rnzndb87d59x1vhr6magy4y46vk98d5x6fhmr6y";
};
}
Ben oui, mais non ...
error: builder for '/nix/store/7lb3z8dxl1hwkmdk0irwkcxal2wbqias-hello-world.drv' failed with exit code 2;
last 10 log lines:
> ./configure: line 1460: can't create config.log: Permission denied
Le configure va vouloir créer des fichiers de même que le make. Résultat, ça ne fonctionne pas. Il faut donner accès au dossier.
Or, rappelez-vous, les sources sont dans le /nix/store qui n'appartient pas à notre utilisateur de build.
la stratégie est donc de copier les sources avant de modifier les droits du dossier pour finalement lancer la procédure à l'intérieur.
Ce qui nous donne au final:
with import (fetchTarball {
url = "https://github.com/NixOS/nixpkgs/archive/release-21.11.tar.gz";
sha256 = "1xk1f62n00z7q5i3pf4c8c4rlv5k4jwpgh0pqgzw1l40vhdkixk9";
}) {};
makeDerivation {
name = "hello-world";
buildPhase = builtins.toFile "builder.sh" ''
cp -r $SOURCE work
chmod -R u+w work
cd work
./configure --prefix $out
make
make test
make install
'';
buildInputs = [ busybox gcc gnumake ];
inherit pkgs;
SOURCE = fetchTarball {
url = "https://github.com/Akanoa/nix-hello/archive/9e363d35a44b00b190a5fa8376dc2d4a221d94a2.tar.gz";
sha256 = "106ra1nd19y57rnzndb87d59x1vhr6magy4y46vk98d5x6fhmr6y";
};
}
Et cette fois-ci notre dérivation fonctionne 😁
$ /nix/store/m9gibyx0yxn4raa4bd0xhrfa6jwxaqm8-hello-world/bin/hello
Hello World!
On peut même se donner un dernier sucre syntaxique.
Il est possible de fusionner deux tableaux
nix-repl> [ 1 ] ++ [ 2 3 4 ]
[ 1 2 3 4 ]
Nous pouvons utiliser cette propriété pour nous simplifier le travail en rendant la busybox
"pré-remplie".
Ne pas oublier de retirer également le champs
buildInputs
du merge avecargs
, sinon le busybox est écrasé.
makeDerivation =
{ pkgs, name, buildPhase, buildInputs ? [], ... } @args : derivation ({
system = "x86_64-linux";
setupPhase = builtins.toFile "setup.sh" ''
PATH=""
for input in $buildInputs; do
PATH="$PATH:$input/bin"
done
'';
builder = "pkgs.bash/bin/bash";
buildInputs = [ pkgs.busybox ] ++ buildInputs;
args = [
"-c"
''
source $setupPhase
source $buildPhase
''
];
} // builtins.removeAttrs args ["pkgs" "buildInputs" ] )
Ce qui permet de ne plus nous en soucier:
with import (fetchTarball {
url = "https://github.com/NixOS/nixpkgs/archive/release-21.11.tar.gz";
sha256 = "1xk1f62n00z7q5i3pf4c8c4rlv5k4jwpgh0pqgzw1l40vhdkixk9";
}) {};
makeDerivation {
name = "hello-world";
buildPhase = builtins.toFile "builder.sh" ''
cp -r $SOURCE work
chmod -R u+w work
cd work
./configure --prefix $out
make
make test
make install
'';
buildInputs = [ gcc gnumake ];
inherit pkgs;
SOURCE = fetchTarball {
url = "https://github.com/Akanoa/nix-hello/archive/9e363d35a44b00b190a5fa8376dc2d4a221d94a2.tar.gz";
sha256 = "106ra1nd19y57rnzndb87d59x1vhr6magy4y46vk98d5x6fhmr6y";
};
}
On pourrait encore rafiner plus la buildPhase
pour écrire encore moins de code, mais nous allons nous arrêter là, nous avons la philosophie de création de dérivation normalisée.
Philosophie que nous appliqueront plus tard avec de meilleurs outils.
Conclusion
Nous avons bien manipulé et même trituré le langage dans tous les sens, mais vous vous doutez bien que ce n'est pas de cette façon quasi artisanale que l'on créé des dérivations dans Nix.
Dans la partie 7, nous verrons comment utiliser stdenv.mkDerivation
qui n'est autre que le pendant de la fonction que nous avons créé mais en bien plus sophistiquée et directement fournie par la librairie standard!
Merci de votre lecture et à la prochaine ❤️
Ce travail est sous licence CC BY-NC-SA 4.0.