Partie 4 : Contraindre une dérivation
Bonjour à toutes et tous! 😀
Quatrième article sur Nix.
Bilan de la situation
Dans la partie 3, nous avons réussi à compiler du C.
Cependant, nous n'avons pas tout fixé, seuls nos sources sont déterministes.
On a deux points flottants:
- le
/bin/sh
on ne sait pas d'où il vient - la dérivation du
gcc
vient d'un<nixpkgs>
dont on ne connait pas la version
Rendre le builder déterministe
Pour cela, nous allons déjà déterminer le /bin/sh
qui est-il? que fait-il dans sa vie?
Générons une dérivation.
nix-repl> derivation {
name = "hello-world";
system = "x86_64-linux";
builder = "/bin/sh";
args = [
"-c"
''
gcc $SOURCE -o $out
''
];
PATH = "${pkgs.gcc}/bin";
SOURCE = builtins.toFile "main.c" ''
#include "stdio.h"
void main() {
printf("Hello World 1\n");
}
'';
}
«derivation /nix/store/i8r1j3d07w4rnzaj7rlsyh08jz2yc04f-hello-world.drv»
Et buildons là en mode verbose à l'extérieur du repl.
|
Là c'est un peu les entrailles de la bête.
Pour éviter tout parasites extérieurs, les builds de Nix sont réalisés dans une sandbox logicielle où tout est contrôlable.
L'une des étapes, consiste à ajouter dans cette sandbox, un exécutable /bin/sh
provenant d'une version de busybox.
Mais cette version, est totalement dépendante de l'installation de Nix pour l'environnement de build et donc dépendant notamment de la version de Nix elle-même.
Pour éviter le fameux, "mais ça marche sur mon laptop".
Nous devons vérouiller ce comportement.
Pour cela nous allons utiliser un bash
provenant d'une dérivation que l'on souhaite devenir fixe.
nix-repl> :b derivation {
name = "hello-world";
system = "x86_64-linux";
builder = "${pkgs.bash}/bin/bash";
args = [
"-c"
''
gcc $SOURCE -o $out
''
];
PATH = "${pkgs.gcc}/bin";
SOURCE = builtins.toFile "main.c" ''
#include "stdio.h"
void main() {
printf("Hello World 1\n");
}
'';
}
This derivation produced the following outputs:
out -> /nix/store/9rsg6zcismripg13mqi4yrmws5i9hq7j-hello-world
Ok, mais nous avons juste caché la poussière sous le tapis.
Le problème reste entier, on ne sait pas ce qu'est <nixpkgs>
, ou plutôt son état dépend de l'état d'installation de Nix.
Ce n'est pas reproductible.
Rendre <nixpkgs> déterministe
Heureusement, il est également possible de fixer cela également.
<nixpkgs>
n'est dans les faits qu'une itération parmis d'autres du projet nixpkgs.
Nix fourni des outils pour rechercher de la donnée venant d'internet, comme fetchGit ou fetchTarball.
nix-repl> builtins.fetchTarball {
url = "https://github.com/NixOS/nixpkgs/archive/refs/tags/22.11.tar.gz";
}
"/nix/store/nkhjmzkf9hky9h34yrfy0cgyd9pbh03v-source"
nix-repl> builtins.fetchTarball {
url = "https://github.com/NixOS/nixpkgs/archive/refs/tags/22.05.tar.gz";
}
"/nix/store/di36mqc6y19ivaa4qjrb2l82c6dqg7m3-source"
On a bien une unicité de l'empreinte selon la version de nixpgs que l'on télécharge.
Voyons un peu ce qu'il y a dedans.
Le dossier pkgs nous intéresse. ^^
Fonction en Nix
Si je vous donne directement la méthode pour récupérer le pkgs de notre tarball vous aller avoir une syncope, donc nous allons y aller progressivement. 😅
Cela, va nous donner un prétexte pour manipuler du Nix et un peu mieux comprendre ce que l'on fera dans les parties suivantes. ^^
D'abord la notion fonction en Nix.
Le x
à gauche des deux-points :
est le paramètre de la fonction. Ce qui est à droite est le corps de la fonction.
Cette fonction est stockée dans la varible inc
.
Qui peut ensuite être appelée et donc évaluée avec une valeur, ici 2.
nix-repl> inc = x : x + 1
nix-repl> inc 2
3
On peut mettre autant de paramètres que l'on veut, il suffit de les séparer par des :
. Le dernier élément sera le corps de la fonction.
nix-repl> inc_by = x : by : x + by
nix-repl> inc_by 4 3
7
En vrai, il s'agit d'une fonction qui renvoit une fonction.
nix-repl> inc_by = x : ( by : x + by )
nix-repl> inc_by 4 3
7
Une fonction peut également prendre un set comme paramètre d'entrée.
Ici un set à deux attributs:
- x
- by
Pour l'appeler, il faut donc lui passer le set correspondant.
{
x = 7;
y = 3;
}
nix-repl> inc_by_object = { x, by } : x + by
nix-repl> inc_by_object { x = 7; by = 3; }
10
Attribut par défaut
Il est également possible d'omettre un paramètre en fournissant un attribut par défaut.
Via la syntaxe
{ x ? 3 } : x
Si x est défini dans le paramètre d'entré, alors il est utilisé, sinon c'est la valeur 3 qui remplace x.
On peut alors utiliser cette propriété:
nix-repl> inc_by_object_optional = { x, by ? 1 } : x + by
nix-repl> inc_by_object_optional { x = 7; by = 3; }
10
nix-repl> inc_by_object_optional { x = 7; }
8
Contexte
Nix, possède une autre particularité, il s'agit des contextes.
let default = 12;
in
{ x ? default } : x
let
définit qu'une variable est déclarée et que celle-ci est disponible dans le contexte suivant le mot-clef in
.
nix-repl> l = let default = 12;
in
{ x ? default } : x
nix-repl> l { x = 3; }
3
nix-repl> l {}
12
Si l'on passe un objet contenant un attribut x
, il est utilisé, si on ne passe rien c'est default
qui sera évalué en tant que x
.
Petit sucre syntaxique.
Il n'est pas nécessaire de stocker la fonction et son contexte dans une variable, nous pouvons l'appeler directement.
nix-repl> ( let default = 12;
in
{ x ? default } : x ) {}
12
nix-repl> ( let default = 12;
in
{ x ? default } : x ) { x = 3; }
3
Import
Enfin, la méthode import
permet de charger les dérivations dans le contexte.
nix-repl> nixpkgs = import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/release-21.05.tar.gz") {}
nix-repl> nixpkgs.pkgs.gcc
«derivation /nix/store/91r1rsrc9mjh8xcppfm071lv8p46a45a-gcc-wrapper-10.3.0.drv»
Le bout du chemin
Si vous avez survécu jusque là, félicitations à vous. 😇
Il est temps de rassembler le tout.
Dans l'ordre:
- on
fetchTarball
les sources à la version voulue - on
import
les dérivations - on stocke dans la variable
nixpkgs
les dérivations - on défini avec
let
etin
un contexte oùnixpkgs
existe - dans ce contexte on créer une fonction qui prend un paramètre optionel
pkgs
qui a pour valeur par défautnixpkgs.pkgs
- le corps de cette fonction est notre dérivation utilisant le paramètre
pkgs
. - on appelle la fonction sans paramètre ce qui a pour effet de déclencher l'utilisation de la valeur par défaut du champ
pkgs
, donc la valeur depkgs == nixpkgs.pkgs
En code ça donne ça:
(let
nixpkgs = import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/release-21.11.tar.gz") {} ;
in
{ pkgs ? nixpkgs.pkgs } :
derivation {
name = "hello-world";
system = "x86_64-linux";
builder = "pkgs.bash/bin/bash";
args = [
"-c"
''
gcc $SOURCE -o $out
''
];
PATH = "pkgs.gcc/bin";
SOURCE = builtins.toFile "main.c" ''
#include "stdio.h"
void main() {
printf("Hello World\n");
}
'';
}){}
On est d'accord, c'est pas top. Mais j'ai pas trouvé plus simple... (c'est faux) 🥲
Par contre, c'est pratique pour tester notre théorie ^^
nix-repl> :b (let
nixpkgs = import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/release-21.05.tar.gz") {} ;
in
{ pkgs ? nixpkgs.pkgs } :
derivation { ... }){}
This derivation produced the following outputs:
out -> /nix/store/15xyifj30dmivl2j5frfym0081wmi7p9-hello-world
nix-repl> :b (let
nixpkgs = import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/release-21.11.tar.gz") {} ;
in
{ pkgs ? nixpkgs.pkgs } :
derivation { ... }){}
This derivation produced the following outputs:
out -> /nix/store/dab6w96pj7vgrw3n64kk4c8d819w08lc-hello-world
Pour deux <nixpkgs>
différents, nous avons bien deux empreintes différentes et reproductibles ! 🎉
On peut cocher tout le monde ^^
Simplifier notre dérivation
Alors en fait, on peut dratisquement simlplifier tout ça, mais je ne l'ai trouvé qu'après coup... ^^'
La première avancée, est qu'on peut se débarasser de la fonction et de l'attribut optionnel.
Opérateur with
En utilisant le mot clef with
, qui a pour rôle de démonter une structure et de mettre chacun de ses attributs dans le contexte courant.
Donc pkgs
de nixpkgs.pkgs
devient disponible après le with
pour le corps de la dérivation.
let
nixpkgs = import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/release-21.11.tar.gz") {};
in
with nixpkgs;
derivation {
name = "hello-world";
system = "x86_64-linux";
builder = "pkgs.bash/bin/bash";
args = [
"-c"
''
gcc $SOURCE -o $out
''
];
PATH = "pkgs.gcc/bin";
SOURCE = builtins.toFile "main.c" ''
#include "stdio.h"
void main() {
printf("Hello World\n");
}
'';
}
Se débarasser du contexte
Et comme nous n'avons pas non plus besoin d'isolation de contexte, on peut également faire sauter le let
et le in
with import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/release-21.11.tar.gz") {};
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");
}
'';
}
Et là tout de suite, ça respire! 😄
J'ai laissé dans l'article le chemin de croix, parce que ça fait manipuler le langage et c'est pas pire, même si le résultat était désastreux. 🤣
PATH = "${pkgs.gcc}/bin";
Peut se remplacer en
PATH = "${gcc}/bin";
Car,
<nixpkgs>
expose directementpkgs
, l'import est donc capable d'aller chercher les dérivations du dossierpkgs
et de les exposer.
Conclusion
Nous avons enfin une dérivation reproductible capable de compiler du C.
Dans la partie 5, nous verrons comment déporter les sources de la dérivations.
Merci de votre lecture ❤️
Ce travail est sous licence CC BY-NC-SA 4.0.