Partie 4 : Contraindre une dérivation

Lecture 6 min. ‱


Table des matiĂšres

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:

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.

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:

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:

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. đŸ€Ł

Remarque

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 ❀