https://lafor.ge/feed.xml

Partie 4 : Contraindre une dérivation

2023-04-20
Les articles de la série

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
missing alt

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.

$ nix-build -vvv /nix/store/zhv7hwhhhql4jk8qklidwi0kdkkj9593-hello-world.drv 2>&1 | grep "sandbox setup: bind mounting"

sandbox setup: bind mounting '/nix/store/7b943a2k4amjmam6dnwnxnj8qbba9lbq-busybox-static-x86_64-unknown-linux-musl-1.35.0/bin/busybox' to '/nix/store/zhv7hwhhhql4jk8qklidwi0kdkkj9593-hello-world.drv.chroot/bin/sh'

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.

missing alt

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.

$ tree -L 1 /nix/store/nkhjmzkf9hky9h34yrfy0cgyd9pbh03v-source
/nix/store/nkhjmzkf9hky9h34yrfy0cgyd9pbh03v-source
├── CONTRIBUTING.md
├── COPYING
├── default.nix
├── doc
├── flake.nix
├── lib
├── maintainers
├── nixos
├── pkgs
└── README.md

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 et in 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éfaut nixpkgs.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 de pkgs == 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 ^^

missing alt

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 directement pkgs, l'import est donc capable d'aller chercher les dérivations du dossier pkgs 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 ❤️

avatar

Auteur: Akanoa

Je découvre, j'apprends, je comprends et j'explique ce que j'ai compris dans ce blog.

Ce travail est sous licence CC BY-NC-SA 4.0.