https://lafor.ge/feed.xml

Partie 6 : Vers une normalisation des dérivations

2023-05-02
Les articles de la série

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.

$ tree /nix/store/4wvz6l7l436ma226k4gky9985fanv8cg-hello-world
/nix/store/4wvz6l7l436ma226k4gky9985fanv8cg-hello-world
└── bin
    └── hello-world

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 input in $buildInputs; do
  PATH="$PATH:$input/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.

mkdir -p $out/bin                       
gcc $SOURCE -o $out/bin/hello-world   

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:

./configure --prefix $out
make
make test
make install

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.

cp -r $SOURCE work
chmod -R u+w work
cd work

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 avec args, 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 ❤️

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.