Partie 6 : Vers une normalisation des dérivations
Lecture 9 min. âą
Table des matiĂšres
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-worldOn 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-worldOn 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"
doneNous allons alors faire deux choses:
- définir un fichier
setup.shvia la mĂȘme mĂ©thode que pour les sources - utiliser la
sourcequi 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:
$setupPhaseanciennement$SETUP$buildPhasele 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 + yIl 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 + yEt maintenant ça marche correctement ^^
nix-repl> f2 data
16Bien 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 + yEt dĂ©sormais y nâest plus obligatoire.
nix-repl> f3 { x = 12; }
12Le 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; } @ argsOn 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
''
];
} // argsIl 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 WorldCâ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'
106ra1nd19y57rnzndb87d59x1vhr6magy4y46vk98d5x6fhmr6yIl est construit via Autotools.
Et donc notre buildPhase sera:
./configure --prefix $out
make
make test
make installNous 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 deniedLe 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 workCe 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
buildInputsdu 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 â€ïž
