Nix, le gestionnaire de paquet
Il est maintenant temps de mettre en pratique le langage Nix pour la gestion de paquet. Pour cela, il y a plusieurs concepts à saisir.
Nixpkgs
Nixpkgs est un dépôt GitHub contenant une collection d'expression Nix en tout genre, dont entre autre :
- Plus de 80 000 paquets
- L'implémentation de NixOS
- Des tonnes de fonctions utilitaires
Le dépôt est versionné et séparé en plusieurs branches, dont notamment :
master
: la branche principale contenant les dernières modifications en date.nixpkgs-unstable
: la version de Nixpkgs en rolling release, comme Arch Linux, correspondant àmaster
après la validation d'une suite de tests.nixos-unstable
: la version denixpkgs-unstable
plus adaptée à NixOS (un peu plus stable que cette dernière, et avec moins de choses liées à macOS).nixos-XX.XX
: les branche stables de NixOS qui sortent deux fois par an : en mai et en novembre. Lorsqu'une version sort, seules des versions mineures ou de bugfix de paquets seront mises à jour.
Nixpkgs est au centre de Nix et de NixOS. Nous reviendrons plus tard dessus, mais nous allons l'utiliser à partir de maintenant donc il était important d'au moins l'introduire.
Les channels
Les channels sont des sources extérieures d'expression Nix, chacun ayant :
- Un nom (en kebab-case de préférence)
- Une URL vers une archive (contenant de préférence des fichiers .nix, mais pas
forcément que ça)
- Si l'URL pointe vers un dossier, Nix ira chercher l'archive
nixexprs.tar.gz
à sa racine.
- Si l'URL pointe vers un dossier, Nix ira chercher l'archive
Chaque utilisateur de Nix a une liste de channel qu'il peut mettre à jour comme
il le souhaite. Les channels de l'utilisateur root
sont les channels système
et sont accessibles par les autres utilisateurs.
Par défaut, après une installation de Nix ou NixOS, un channel a automatiquement été ajouté :
- Sur NixOS, un channel système (donc sur l'utilisateur
root
) a été ajouté appelénixos
et pointant vershttps://nixos.com/channels/nixos-XX.XX
, leXX.XX
correspondant à la version de NixOS que vous avez installée (qui peut donc êtreunstable
). - Sur Nix, un channel utilisateur (donc sur l'utilisateur ayant installé Nix) a
été ajouté appelé
nixpkgs
et pointant vershttps://nixos.org/channels/nixpkgs-unstable
. Il est normal d'utiliser le channelunstable
, étant pratique dans le cas d'un Nix seul d'avoir une version de Nixpkgs qui évolue rapidement. Ça ne devrait normalement pas poser de problème, mais il est toujours possible de passer sur un channel stable si vous le souhaitez (nixos-XX.XX
sur Linux ounixpkgs-XX.XX-darwin
sur macOS).
Manipuler les channels
La commande nix-channel
permet de manipuler les channels. Par défaut, elle
cible les channels de l'utilisateur courant, il suffit donc de l'appeler avec
sudo
pour manipuler les channels systèmes (ceux de root
).
nix-channel --list
: Liste les channels enregistrés.nix-channel --add <url> [name]
: Ajoute un channel (si le nom n'est pas précisé, il sera déduit du chemin).nix-channel --remove <name>
: Supprime un channel.nix-channel --update
: Télécharge à nouveau l'archive des channels, mettant à jour leur contenu.
Pour rappel, si vous êtes sur NixOS, c'est un channel système qui a été ajouté à
l'installation. Sans sudo
, la commande nix-channel --list
ne vous listera
donc rien.
À la manière des listes de paquets des autres gestionnaires de paquet, Nix télécharge une copie des channels sur votre ordinateur. Quand vous invoquez un channel, Nix ira chercher dans cette copie locale, ce qui en plus de gagner du temps permet de ne pas dépendre constamment de votre connexion internet.
En revanche, cela signifie que les channels ne sont pas mis à jour
automatiquement. Pensez donc à appeler nix-channel --update
régulièrement pour
mettre à jour la copie locale de vos channels.
Après avoir ajouté un channel, il faut le télécharger une première fois avec un
nix-channel --update
. Sans ça il ne sera pas possible de l'utiliser.
NIX_PATH
C'est sympa les channels, mais comment on s'en sert ?
Nix utilise une variable d'environnement appelée NIX_PATH
qui est une liste de
chemins vers des expressions Nix, possiblement nommés, utilisables depuis
n'importe où à la manière du PATH
.
Son format est le suivant : name1=path1:path2:name3=path3:etc.
où name
est
le nom (optionnel) de l'alias et path
le chemin vers lequel il pointe.
Si vous êtes sur NixOS, essayez d'afficher la valeur de NIX_PATH
dans votre
shell :
echo $NIX_PATH
Vous verrez normalement que la variable contient au moins un alias, nixpkgs
,
qui pointe vers un dossier sur votre machine : celui du channel nixos
. Ça
permet de pouvoir appeler le channel autant via le nom nixos
que nixpkgs
.
Vous devriez aussi voir un alias vers votre configuration NixOS, et possiblement
plus.
Si vous n'êtes pas sur NixOS, cette variable sera sûrement vide. C'est, car
depuis peu, lorsque la variable NIX_PATH
n'est pas définie,
Nix la définie automatiquement par les dossiers contenant vos channels et celui du système.
Mais du coup, à quoi sert cette variable ?
Comme mentionné plus haut, vous pouvez utiliser partout les expressions
contenues dans les dossiers du NIX_PATH, et ça grâce à une syntaxe très simple :
il suffit d'entourer de chevron le nom d'un chemin relatif à un dossier du
NIX_PATH. Par exemple, pour importer votre channel nixpkgs
(et donc le fichier
default.nix
à sa racine), il suffit d'écrire :
import <nixpkgs>
On pourrait aussi écrire un chemin vers un fichier précis :
import <nixpkgs/pkgs/top-level/all-packages.nix>
Cette syntaxe va beaucoup nous servir pour la suite.
En évaluant ces expressions dans le REPL, vous remarquerez que ce sont des fonctions. Vous pouvez les évaluer, mais je vous déconseille très fortement d'essayer d'afficher leur valeur de retour dans le REPL.
Vous comprendrez pourquoi un peu plus tard.
Pour importer les attributs d'un ensemble dans le scope global du REPL, vous
pouvez utiliser la commande REPL :l
. Cet exemple devrait vous afficher la
version de Nixpkgs que vous utilisez :
:l <nixpkgs>
lib.version
La plupart des commandes de nix acceptent un argument -I
(comme Include)
permettant de surcharger le NIX_PATH
.
Les dérivations
La notion de dérivation est probablement la plus importante de Nix. C'est à la base de la gestion de paquet, et c'est quelque chose que vous aurez beaucoup à manipuler.
Une dérivation est la définition d'un build, qui prend des entrées et produit
des sorties. Concrètement, c'est un set habituellement construit grâce à la
fonction stdenv.mkDerivation
de Nixpkgs, rempli d'attributs décrivant un
processus de build.
En vérité, une dérivation est construite avec la fonction builtin derivation
,
mais il faut lui donner des paramètres complets sur le système cible, la
toolchain à utiliser, etc. stdenv.mkDerivation
appelle derivation
en lui
donnant tout ce qu'il faut pour nous.
Nix n'a pas de notion de paquet à proprement parler, en vérité les paquets sont simplement des dérivations. Ça ne veut pas dire qu'une dérivation est une façon Nix-euse de dire paquet, une dérivation peut correspondre à bien plus de chose : un fichier de configuration généré, un patch téléchargé pour un build, en bref n'importe quoi qui produit des fichiers (ou même un seul fichier).
Notre première dérivation
Essayons d'écrire une dérivation simple. Notre but va être de compiler ce Hello World basique contenant un Makefile.
Nous allons utiliser la fonction stdenv.mkDerivation
contenue dans Nixpkgs,
que nous devons d'abord importer. Le fichier default.nix
à la racine nixpkgs
renvoie une fonction dont l'évaluation nous donne un set contenant la plupart
des choses qu'expose Nixpkgs, dont stdenv
.
La fonction en question prend un set en paramètre, dont les attributs servent à personnaliser le comportement de Nixpkgs. Nous pouvons laisser tout par défaut et donc simplement donner un set vide.
stdenv.mkDerivation
prend aussi un set en paramètre, avec beaucoup d'attributs
possibles, dont par exemple :
pname
: Le nom de la dérivationsrc
: Les sources de la dérivationbuildInputs
: Les dépendances de la dérivation (grossièrement)nativeBuildInputs
: Les dépendances de build de la dérivation (grossièrement)buildPhase
: Le script de build de la dérivationinstallPhase
: Le script d'installation de la dérivationmeta
: Les métadonnées de la dérivation (mainteneur, license, etc.)- et bien plus...
Il existe un attribut name
qui est déprécié, il est préférable d'utiliser
pname
et version
à la place, qui vont être concaténé pour donner un nom sous
la forme (pname)-(version)
.
Dans un fichier hello.nix
, créons une dérivation pour notre Hello World :
let
nixpkgs = import <nixpkgs> { };
in
nixpkgs.stdenv.mkDerivation {
pname = "hello-world";
version = "1.0.0";
src = nixpkgs.fetchgit {
url = "https://github.com/Litarvan/hello-world";
rev = "6a12c0a8f7f5fde57fe4a1ea7eda0afad30877b1";
hash = "sha256-RkBBXNt5qqDEE0wydMxPbYnpOIXuVMtMyWDmoeNW4NU=";
};
buildPhase = ''
make
'';
installPhase = ''
mkdir -p $out/bin
make install PREFIX=$out
'';
}
Il y a plusieurs choses à saisir ici :
- On utilise la fonction
nixpkgs.fetchgit
et nonbuiltins.fetchGit
pour télécharger les sources du projet. La différence est quebuiltins.fetchGit
télécharge depuis git au moment de l'évaluation, alors quenixpkgs.fetchgit
le fait au moment du build. Celle debuiltins
peut être utilisée pour importer directement des fichiers.nix
depuis un dépôt Git et les évaluer, mais n'est pas très adaptée pour les sources d'une dérivation où là, c'est plutôtnixpkgs.fetchgit
qui est préférée. En vérité, cette dernière retourne elle-même une dérivation dont le build télécharge les sources depuis git, et dont l'output est le dossier contenant les sources. De même, il vaut mieux utilisernixpkgs.fetchurl
pour télécharger des sources depuis une URL plutôt quebuiltins.fetchurl
. buildPhase
etinstallPhase
sont des scripts bash où des variables et fonctions sont mises à disposition. Par exemple$out
est le dossier de sortie de la dérivation, où ses fichiers doivent être installés.- Cet Hello World est générique et est fait pour les distributions Linux
classiques,
make install
va donc installer les fichiers dans/usr/local
. Pour éviter ça, nous surchargeons la variablePREFIX
du Makefile pour qu'elle pointe vers$out
à la place. - On passe à
fetchgit
une révision git (ici le hash du commit cible) pour s'assurer que notre dérivation soit reproductible : qu'elle donne toujours le même résultat. Utilisermaster
casserait ce principe, risquant de changer le résultat si un nouveau commit venait à être push sur le dépôt. - On passe aussi à
fetchgit
un hash du résultat, permettant de s'assurer de la validité des sources téléchargées. Il est obligatoire de le passer, encore une fois pour s'assurer de la reproductibilité de la dérivation.
Pour build notre dérivation, vous avez deux choix :
- nix-build
- REPL
Pour build notre dérivation avec nix-build
, exécutez simplement la commande
suivante dans le terminal :
nix-build hello.nix
Pour build notre dérivation avec le REPL, vous pouvez utiliser la commande REPL
:b
:
:b import ./hello.nix
Dans les deux cas, cela va déclencher le build de la dérivation, et vous donnera
un chemin vers le dossier contenant son résultat. Si vous avez utilisé
nix-build
, un lien symbolique result
pointant vers ce dossier sera créé dans
le dossier courant.
Dans la dérivation, j'utilise la syntaxe let ... in
pour définir une variable
locale nixpkgs
, pour ne pas avoir à l'importer deux fois.
Une façon courante de faire et plutôt d'utiliser with
en haut du fichier, pour
pouvoir utiliser directement stdenv
et fetchgit
sans préfixe. Essayez de
changer la dérivation pour utiliser cette syntaxe à la place.
Ici, notre dérivation a produit un dossier, mais la sortie d'une dérivation peut aussi directement être un fichier.
Évaluation et build
Il est important de différencier les trois étapes de la vie d'une dérivation :
- Sa déclaration : Lorsque l'on écrit l'appel à
stdenv.mkDerivation
qui la définit. - Son évaluation : Lorsque l'on demande à Nix d'évaluer la déclaration de la
dérivation, il la transforme alors en une dérivation "prête à être buildée".
- Lorsque c'est le cas, la plupart du temps, Nix va créer un fichier
.drv
qui contient toutes les informations de la dérivation (ses dépendances, sa source, son dossier de sortie, etc...).
- Lorsque c'est le cas, la plupart du temps, Nix va créer un fichier
- Son build : Lorsque l'on demande à Nix de build la dérivation (via
nix-build
,nix-env
, etc.). Il va alors télécharger les sources, build le projet, et installer le résultat dans le dossier de sortie.
Nix fait beaucoup de chose lors de son évaluation, il doit évaluer tout ce qui
est nécessaire pour définir clairement les entrées de la dérivation, pour au
final écrire toutes ces informations dans le fichier .drv
.
De ce fait, si vous tentez d'évaluer Nixpkgs dans le REPL et d'afficher le set retourné dedans, ce dernier tentera d'afficher les valeurs qui contient. Il devra pour ça les évaluer, et donc faire toute l'étape d'évaluation de toutes les dérivations contenues dans Nixpkgs.
En bref, vous en aurez pour beaucoup de temps, et votre processeur ainsi que votre espace disque se le rappelleront, tout ça pour pas grand-chose.
C'est aussi pour ça qu'il vaut mieux passer par nixpkgs.fetchgit
et
nixpkgs.fetchurl
plutôt que builtins.fetchGit
et builtins.fetchurl
pour
les sources des dérivations. En passant par les builtins
, leurs sources
seraient téléchargées lors de leur évaluation, et non du build. De ce fait,
évaluer une dérivation serait bien plus long et coûteux.
Avec Nix, tout est fait à l'évaluation. Les dérivations sont les seules exceptions, et unique moyen de différer une action. Cette distinction est importante à comprendre.
L'évaluation d'une dérivation est simplement la création de sa "recette" de build.
Il est possible de clairement voir les étapes d'évaluation et de build en
faisant manuellement ce que font nix-build
et :b
.
Commencez par évaluer la dérivation :
nix-instantiate hello.nix
Cela devrait vous afficher le chemin du
.drv
crééDemandez ensuite au store de build (ou "réaliser") la dérivation évaluée :
nix-store --realise /nix/store/nom-du-drv-hello.drv
Cela devrait vous afficher le chemin du dossier de sortie de la dérivation.
Le store
Vous l'avez sûrement vu, mais le chemin du résultat de tout à l'heure fait un peu peur. C'est pour une bonne raison.
Nix stocke toutes les dérivations et leurs sorties dans un dossier appelé le
store : /nix/store
, chacun ayant son propre dossier à l'intérieur.
Dans ce dossier, qui est en lecture seule pour tous les utilisateurs y compris
root (seul Nix peut le modifier en ajoutant ou supprimant une dérivation),
chaque dérivation est nommée par un hash unique calculé à partir de ses entrées.
De ce fait, si deux appels à mkDerivation
s'évaluent en deux dérivations
identiques, elles auront le même hash, pointeront donc vers le même fichier
.drv
et donc la même dérivation ainsi que le même dossier de sortie.
Nix est assez intelligent pour savoir que si le dossier de sortie d'une
dérivation existe déjà, il est inutile de la rebuild. En effet, le build d'une
dérivation est censé être pur : les mêmes entrées donneront le même résultat.
Donc comme le dossier de sortie contient un hash calculé à partir desdites
entrées, s'il existe déjà, alors un build donnerait exactement la même chose et
serait donc inutile (et c'est pareil pour les .drv
!).
stdenv ?
Parlons rapidement de ce qu'est stdenv
.
Diminutif de standard environment
, stdenv
est un gros set (et plus
précisément une dérivation) correspondant à une toolchain C/C++ complète pour le
système et l'architecture de l'hôte. Il contient notamment :
- Un compilateur C
- Un compilateur C++
- Des coreutils (cp, mv, rm, etc.)
- Make
- Un linker
- Des gestionnaires d'archive (xz, gzip, etc.)
- Et plus encore.
En bref, il contient les outils nécessaires à la compilation d'un programme C/C++ standard.
Sur Linux, stdenv
correspond à une toolchain GNU (basée sur GCC). Sur macOS en
revanche, c'est une toolchain Clang qui est utilisée.
stdenv
pointe vers la toolchain par défaut, mais il en existe d'autres. Par
exemple, pour utiliser une toolchain GCC sur macOS, vous pouvez utiliser
gccStdenv
(ce qui est utile pour Tiger, qui utilise des extensions GNU !), et
inversement il existe clangStdenv
. Il existe aussi stdenv_32bit
qui est une
toolchain multilib supportant le 32 bits.
Une documentation plus complète de stdenv
est disponible
à cette adresse.
Les hooks de Nixpkgs
Nixpkgs contient énormément de 'hooks', des dérivations qu'on peut mettre dans
le champ nativeBuildInputs
d'une dérivation et qui vont se greffer sur le
processus de build pour ajouter des fonctionnalités.
Par exemple, ajouter autoreconfHook
dans les nativeBuildInputs
va, en plus
d'ajouter Autoconf, Automake et Libtool aux dépendances de build, activer un
hook qui détectera, compilera et installera automatiquement le projet Autotools
détecté dans les sources de la dérivation, sans rien avoir à préciser dans les
phases différentes phases !
Si on reprend notre exemple de tout à l'heure, mais sur
la branche autotools,
on peut se contenter d'utiliser le autoreconfHook
et tout sera automatique :
let
nixpkgs = import <nixpkgs> { };
in
nixpkgs.stdenv.mkDerivation {
pname = "hello-world";
version = "1.0.0";
src = nixpkgs.fetchgit {
url = "https://github.com/Litarvan/hello-world";
rev = "1518ae79f1198b9ac1ec55d86f6d72bd3aeb3e1d";
hash = "sha256-3XV35+vfboMV0JlClaeFiKRuhXRxAoREhrtwIXOii1U=";
};
# Il est courant d'utiliser la syntaxe 'with' ici pour facilement lister
# plusieurs dépendances.
nativeBuildInputs = with nixpkgs; [ autoreconfHook ];
}
Essayez de build cette dérivation !
La liste complète des hooks est disponible à cette adresse.
Pour aller plus loin : les builders
Par défaut, Nix utilise un très gros script bash
pour build les dérivations.
C'est ce dernier qui appelle les différentes phases de build (buildPhase
,
installPhase
, etc.), et qui défini leur comportement par défaut.
Nous avons utilisé très peu de sa puissance, mais il permet d'automatiser
beaucoup de choses pour nous. Par exemple, s'il détecte la présence d'un fichier
configure
, il saura qu'il faut le lancer ainsi que make
pour build le
paquet. Il aurait donc été possible dans ce cas de ne définir aucune build
phase, et de laisser le script tout déduire pour nous.
Ce script, c'est le builder par défaut : stdenv.setup
.
En vérité, lorsqu'on appelle stdenv.mkDerivation
, c'est le builtin
derivation
qui est appelé avec plusieurs paramètres ajoutés aux nôtres :
system
: le système cible pour lequel définir la dérivation, couple de l'architecture et du système d'exploitation (commex86_64-linux
ouaarch64-darwin
). La stdenv par défaut passera le système de l'hôte :builtins.currentSystem
.builder
: le builder à utiliser pour build la dérivation. Ce dernier doit être un chemin vers un programme, et stdenv passera par défaut celui de bash.args
: la liste d'arguments à passer au builder. stdenv passera par défaut le chemin vers le scriptdefault-builder.sh
, ce dernier appelant simplement lesetup
ainsi que sa fonctiongenericBuild
.- Et bien plus ! Il est évidemment possible de surcharger ces paramètres dans
mkDerivation
.
Le programme builder reçoit en variable d'environnement tous les attributs de
l'évaluation (name
, system
, out
, buildPhase
, etc.), c'est comme ça que
le builder générique peut appeler les phases que vous avez définies, et que vous
pouvez aussi utiliser n'importe lesquels de vos attributs dans vos phases de
build.
Il serait très bien possible d'écrire un builder dans un autre langage, comme Python !
Il est aussi possible de définir des variables d'environnement en tant qu'attributs de la dérivation et certaines d'entre elles seront reconnues par Nix.
Par exemple, définir l'attribut NIX_CFLAGS_COMPILE
permettra de rajouter des
flags de compilation.
Pour plus d'information, vous pouvez parcourir
le dossier generic
des stdenv
,
qui contient tout ce qui est partagé entre les différentes toolchains (notamment
la définition du builder générique, et de mkDerivation
).
Installer des paquets avec nix-env
Passons maintenant à l'installation de paquets (enfin !).
Nix inclut une commande nix-env
permettant de gérer les environnements d'un
utilisateur (user environments). Un environnement utilisateur est une
dérivation dont la sortie est celle d'un ensemble de paquet. Il peut y avoir
plusieurs environnements disponibles, et l'utilisateur peut en désigner un comme
étant actif, sa sortie sera alors liée à au dossier ~/.nix-profile
de
l'utilisateur.
En bref, nix-env
permet d'installer des paquets, et les fichiers desdits
paquets se retrouveront dans ~/.nix-profile/bin
, ~/.nix-profile/lib
, etc.
Étant donné que ~/.nix-profile/bin
est dans le PATH
, les paquets installés
avec nix-env
sont donc accessibles depuis le terminal, comme avec n'importe
quel gestionnaire de paquet.
nix-env
a plusieurs sous-opérations, notamment :
-i
: Pour installer un paquet (ou plusieurs)-u
: Pour mettre à jour un paquet (ou plusieurs)-e
: Pour désinstaller un paquet (ou plusieurs)-q
: Pour lister les paquets installés- et plus ! (regardez le
--help
pour plus d'informations)
Il est en théorie possible d'installer un paquet avec la commande suivante :
nix-env -i <nom-du-paquet>
Mais en pratique, cette commande est particulièrement lente. En effet, Nix ne connait pas le nom des dérivations tant qu'il ne les évalue pas. Pour trouver à quelle dérivation correspond le nom du paquet que vous avez mis, il va devoir évaluer chacun de leur nom (heureusement, pas besoin d'évaluer les dérivations en entier, c'est la magie de l'évaluation lazy !). À titre d'exemple, cela prend environ 15 secondes sur ma machine AMD.
Pour éviter cela, il est possible de spécifier à la place une expression Nix
correspondant à la dérivation du paquet que vous voulez installer. C'est le
paramètre -A
(comme attribute) qui permet de faire ça.
L'expression Nix que vous donnez à -A
a par défaut accès à des variables
nommées comme chacun de vos channels, donc pour installer 'sl' vous pouvez faire
la commande suivante :
- Nix
- NixOS
nix-env -iA nixpkgs.sl
nix-env -iA nixos.sl
Ce n'est pas un oubli de ma part, il n'y a effectivement pas besoin d'utiliser
sudo
.
En effet, les gestionnaires de paquets classiques installent leurs fichiers dans
les dossiers du système, nécessitant soit de les installer en root, soit de
modifier les droits desdits dossiers (comme fait homebrew
, et ce n'est
vraiment pas bien).
Nix ne fonctionne pas comme ça, et ne fera rien de plus que de construire des
dérivations dans le store ainsi que modifier le lien symbolique de
~/.nix-profile
, cela ne nécessite donc pas de droits particuliers. C'est
encore une fois un de ses nombreux avantages.
La sortie devrait ressembler à quelque chose du genre :
Si c'est la première fois que vous utilisez nix-env
, il est possible que
beaucoup de dépendances se soient installées. C'est normal, ce sont des
dépendances courantes qui sont utilisées par de nombreux paquets, et ne seront
sûrement pas installées à nouveau par la suite.
Essayons de comprendre ce qu'il s'est passé :
- D'abord, Nix a évalué l'expression
nixpkgs.sl
(ounixos.sl
), correspondant à l'attributsl
de l'évaluation du channelnixpkgs
(ounixos
). Cela a donné une dérivation dont il a affiché le nom. - Ensuite, il a listé les sorties de dérivation nécessaires à obtenir : celles des dépendances, mais aussi celles du paquet lui-même. Il a gardé seulement celles qui n'étaient pas déjà trouvables dans le store (donc sur le screenshot, uniquement celle du paquet lui-même). Il différencie celles qu'il devra build (built) de celles qu'il pourra télécharger depuis le cache (fetched).
- Pour chaque sortie, si elle était disponible dans le cache, alors il la téléchargeait depuis ce dernier (ce qu'il s'est passé sur le screenshot). Sinon, il aurait build la dérivation lui-même (la compilant donc).
- Ensuite, il l'a ajouté à un nouvel environnement utilisateur contenant le
nouveau paquet, mais aussi le contenu de l'ancien environnement actif. Il
build donc ce nouvel environnement final
building '/nix/store/....-user-environment.drv'...
. - Pour finir, il a défini cet environnement comme le nouvel environnement
actuel, le liant à la place de l'ancien dans le dossier
~/.nix-profile
.
Un fichier ~/.nix-profile/bin/sl
a donc dû apparaître, et vous pouvez
maintenant pouvoir lancer sl
depuis le terminal.
C'est la commande nix-store --realise
, utilisée pour build (réaliser) une
dérivation, qui va automatiquement chercher dans le cache
(https://cache.nixos.org/) si la dérivation au hash
donné est disponible. Si c'est le cas, plutôt que de la build elle-même, elle
ira la télécharger depuis le cache.
Le dossier contentant les variables accessibles via -A
sont dans
~/.nix-defexpr
. Par défaut, l'expression donnée à nix-env -A
est donc
évaluée avec comme variables disponibles celles des sets des expressions
définies dans ~/.nix-defexpr/*/*.nix
. D'où le nom defexpr
(default
expression). Il est possible d'utiliser le paramètre -f
pour spécifier un
fichier ou dossier différent à la place.
Par exemple, vous pouvez utiliser nix-env
pour installer notre dérivation de
tout à l'heure :
nix-env -if hello.nix
Cette fois, Nix devrait afficher qu'il a besoin de build certaines
dérivations, ne les ayant pas trouvées dans le cache. Il devrait au moins
afficher la dérivation hello-world
, mais aussi celles de ses sources,
nixpkgs.fetchgit
produisant une dérivation.
Nous n'avons pas eu besoin de préciser un attribut via -A
car la dérivation
était directement le retour du fichier hello.nix
. Si le fichier contenait à la
place un set, nous aurions pu spécifier quel attribut choisir à l'intérieur avec
-A
.
En revanche, il faut bien différencier les default expressions du NIX_PATH
.
Sur NixOS, malgré l'alias dans le NIX_PATH
, vous ne pourrez pas utiliser
nixpkgs
dans -A
car il n'est pas défini dans ~/.nix-defexpr
.
Les générations
Comme mentionné plus haut, nix-env
permet de gérer non pas un seul
environnement, mais plusieurs. Chaque appel à nix-env
ne modifie pas
l'environnement actuel, mais en créé à la place un nouveau basé sur le précédent
et le défini comme étant actif.
Ces environnements sont appelés des générations. Vous pouvez voir la liste des générations avec la commande suivante :
nix-env --list-generations
Seul l'environnement actif est lié dans le dossier ~/.nix-profile
. Il est
possible de basculer d'un environnement actif à un autre en utilisant la
commande suivante :
nix-env --switch-generation <numéro de la génération>
En un instant, vous reviendrez donc à un état antérieur. Nix permet ainsi de CTRL-Z son environnement !
Mettre à jour des paquets
Si vous voulez mettre à jour des paquets, il faut d'abord penser à mettre à jour vos channels :
- Ceux de votre utilisateur (notamment si vous êtes sur Nix) :
nix-channel --update
- Ceux de votre système (notamment si vous êtes sur NixOS) :
sudo nix-channel --update
Vous pouvez ensuite utiliser :
nix-env -iA expression-du-paquet
: pour réinstaller (et donc possiblement mettre à jour) un paquet (Nix remplacera l'ancien automatiquement).nix-env -uA expression-du-paquet
: la même chose, mais ne fonctionnera que si vous avez déjà installé le paquet, et ne fera rien s'il est déjà à jour.nix-env -u nom-du-paquet
: pour le faire via le nom de la dérivation plutôt que son expression (ce qui prendra un peu de temps).nix-env -u
pour mettre à jour tous les paquets installés vianix-env
à la fois (cela prendra aussi un petit moment le temps que Nix évalue tout, mais ça peut valoir le coup).
Lorsqu'une dérivation est installée dans votre environnement actif, elle maintient en vie toutes ses dépendances dans le store.
Si vous ne mettez pas à jour tous vos paquets, les anciennes versions de leurs dépendances seront donc encore présentes dans votre système, ce qui peut prendre une certaine quantité de place surtout si vous accumulez ça plusieurs fois.
Si vous êtes sur Nix, vous pouvez mettre à jour Nix à l'aide de nix-env
:
nix-env -uA nixpkgs.nix
Sur NixOS, Nix est mis à jour par nixos-rebuild
(voir la partie sur NixOS).
Supprimer des paquets
Pour supprimer des paquets, vous pouvez utiliser la commande suivante :
nix-env -e nom-du-paquet [nom-du-paquet-2...]
Comme vous allez supprimer des dérivations déjà installées, c'est par leur nom que vous allez les identifier et non leur expression (dont Nix ne se rappelle même pas).
Si vous testez cette commande, vous remarquerez qu'elle est instantanée.
C'est normal, en vérité Nix n'a pas supprimé les fichiers, mais simplement supprimé la dépendance de l'environnement (en créant donc un nouveau) et donc supprimé ses liens symboliques, mais en vérité la dérivation et ses dépendances sont toujours présentes dans le store. Nous verrons un peu après comment les supprimer.
La documentation complète de nix-env
est disponible
à cette adresse.
Des liens partout
Comme vous l'avez remarqué, Nix utilise beaucoup de liens symboliques. Tout ce
que produit Nix se trouve dans /nix/store
avec un dossier pour chaque élément,
le reste est quasi exclusivement constitué de liens symboliques vers le store.
Si vous vous rappelez la piscine de C, vous vous souvenez peut-être que les binaires dits dynamiques ont le chemin des bibliothèques dont ils dépendent écrits à l'intérieur. À quoi ça ressemble sur Nix ?
Si je lance otool -L
sur macOS (ou ldd
sur Linux) sur un binaire dynamique
installé par Nix pour lister les bibliothèques dont il dépend, je vois ceci :
Tout (ou presque, mais ça serait tout sur NixOS) pointe vers d'autres
dérivations dans /nix/store
. En effet, là où les binaires classiques cherchent
simplement leurs dépendances dans /lib
, /usr/lib
, /usr/lib64
, etc., les
binaires installés ou compilés par Nix ont écrit à l'intérieur le chemin précis
vers leurs dépendances exactes.
Ce principe, couplé à celui que chaque dérivation ait son propre dossier, permet notamment d'avoir plusieurs versions de la même librairie (ou de n'importe quel paquet) installées en même temps, et que chaque programme puisse utiliser la version dont il a besoin. C'est un énorme avantage que Nix offre par rapport aux autres gestionnaires de paquets.
Malheureusement, sur NixOS, c'est aussi un inconvénient. Comme tout sur NixOS
est géré par Nix, tout est dans le store. Il n'y a donc pas de dossier
/usr/lib
sur NixOS. De ce fait, si vous téléchargez un binaire pré-compilé, il
ne trouvera pas les dépendances dont il a besoin et ne fonctionnera donc pas
sans modification.
Le garbage collector
Entre les sorties des dérivations et les dérivations elles-mêmes (autant celles
correspondant à des paquets que les dérivations intermédiaires) le store se
remplit très vite. Tentez de faire un ls -l /nix/store
pour voir à quel point.
Mais du coup, quand-est-ce que les dérivations sont supprimées ? Et les anciens environnements utilisateurs ?
Effectivement, le store peut finir par devenir très gros. La moindre dérivation est mise dedans sans être enlevée après, et les anciens environnements utilisateurs sont gardés. Heureusement, il existe le garbage collector de Nix, qui va supprimer les dérivations qui ne sont plus utilisées et les environnements non-actifs.
Pour savoir ça, il passe en revue les dérivations qui sont dans le store, il
analyse leur .drv
, et supprime simplement celles qui ne sont pas référencées
comme entrée par une autre dérivation, et qui ne sont pas non plus liées
symboliquement à un dossier hors du store (par exemple dans un result
produit
par nix-build
).
Pour lancer le garbage collector, il suffit de lancer la commande suivante :
sudo nix-collect-garbage -d
Si besoin, il est possible de configurer le garbage collector pour se lancer automatiquement à intervalles réguliers.
Les dérivations qui sont conservées par le garbage collector (notamment celles
qui sont liées à des dossiers hors du store) sont appelées des roots
(racines) et sont situées dans le dossier /nix/var/nix/gcroots
. Il est
possible de les lister en regardant notamment le contenu des dossiers
/nix/var/nix/gcroots/auto
et /nix/var/nix/gcroots/profiles/per-user/$USER
:
ls -l /nix/var/nix/gcroots/{auto, profiles/per-user/$USER}
Essayez pour voir ! Le dossier auto
contiendra les liens vers des result
produits par nix-build
ou similaire (s'il n'y en a jamais eu, le dossier
n'existera pas), et celui dans per-user
contiendra les liens vers les
dérivations de vos itérations d'environnement utilisateur (qui, même en étant
des racines, seront supprimées par le garbage collector si elles ne sont pas
actives) ainsi que vos channels.
Tout à l'heure, nous avons détaillé le fonctionnement de nix-build
. Ce dernier
aurait en vérité exécuté comme première commande :
nix-instantiate --add-root result --indirect hello.nix
--add-root result
aurait créé un lien symboliqueresult
vers le.drv
dans les racines du garbage collector, évitant qu'elle ne soit supprimée par ce dernier.--indirect
aurait créé le lienresult
dans le dossier courant en plus de celui dans le dossier des racines, ce dernier pointant versresult
qui lui pointe vers la dérivation. De ce fait, la suppression deresult
entraînerait la suppression de la racine.
Nixpkgs en détail
Nixpkgs expose plus de 80 000 paquets que vous pouvez parcourir à l'aide du site search.nixos.org. Le dépôt GitHub est très actif et particulièrement ouvert aux contributions, alors n'hésitez pas à ouvrir des PRs ! C'est beaucoup moins dur qu'il n'y paraît !
Si vous voulez comprendre comment fonctionne un paquet et comment l'utiliser,
n'hésitez pas à cliquer sur le bouton Source
sur la page de recherche, elle
vous amènera à l'endroit dans Nixpkgs où est définie la dérivation du paquet.
Vous pourrez voir comment est compilé le paquet, mais aussi quelles options il
peut accepter.
En effet, sur Nixpkgs, les dérivations sont presque toutes définies dans des fonctions, prenons par exemple la dérivation de 'sl', on peut voir au début :
{ lib, stdenv, fetchFromGitHub, ncurses }:
stdenv.mkDerivation rec {
# ...
}
En effet, les dérivations situées dans Nixpkgs ne peuvent pas simplement
importer la racine de Nixpkgs pour accéder aux attributs dont elles ont besoin.
A la place, les dérivations sont donc rangées dans des fonctions qui prennent un
set d'attributs en paramètre contenant tout ce dont elles ont besoin, ici lib
,
stdenv
, fetchFromGitHub
et ncurses
.
Cette fonction sera ensuite appelée dans nixpkgs, probablement dans
la liste générale des paquets,
en lui passant ce dont elle a besoin. Mais il serait plutôt contraignant de
passer tout ce qu'il faut à chaque fois, il existe donc une fonction
callPackage
définie par Nixpkgs qui passe automatiquement à la fonction ce
dont elle a besoin (en analysant le nom des attributs qu'elle déstructure) parmi
tous les trucs possibles et imaginables (les fonctions utiles, les autres
paquets, la stdenv, etc.) à une fonction, pour qu'elle puisse prendre ce qu'elle
veut.
Comment fait la fonction callPackage
pour savoir quels attributs passer à la
fonction ? Elle utilise la fonction builtin builtins.functionArgs
qui retourne
un set listant chaque attribut, ainsi que s'ils ont une valeur par défaut.
Les overrides
L'avantage de ces fonctions est que l'on peut modifier les valeurs qu'elles prennent, et c'est là l'une des très grandes forces de nix : la possibilité de customiser les paquets.
Dans le cas de
'sl',
imaginons que je veuille lui donner un autre ncurses à la place. Je peux pour ça
utiliser override
:
with import <nixpkgs> { };
sl.override {
ncurses = ncurses5; # ncurses5 est une version plus ancienne de ncurses, sl utilise normalement la version 6
}
Cela va créer une nouvelle dérivation basée sur celle de 'sl', mais qui utilise ma propre version de ncurses à la place. Ici, les inputs de la dérivation vont changer et donc son hash aussi, Nix va donc compiler localement la dérivation vu qu'il ne la trouvera pas dans le cache, et il la différenciera du 'sl' original.
Pour installer cette version via nix-env
, il est possible écrire le code dans
un .nix
puis d'utiliser nix-env -f
. Il est aussi possible d'utiliser
nix-env -E
qui prend en paramètre l'expression d'une fonction renvoyant une
dérivation, à laquelle il passe les attributs des expressions par défaut
(notamment nos channels !) :
nix-env -iE "{ nixpkgs, ... }: with nixpkgs { }; sl.override { ncurses = ncurses5; }"
Essayez de lancer cette commande, vous devriez voir Nix recompiler 'sl' avec la version 5 de ncurses.
override
permet donc de modifier les valeurs des attributs d'entrée d'une
fonction de dérivation définie dans Nixpkgs. Mais il existe aussi un moyen de
modifier les attributs finaux de la dérivation : overrideAttrs
.
Par exemple, imaginons que je veuille modifier la source de 'sl', il me suffit de faire :
with import <nixpkgs> { };
sl.overrideAttrs (oldAttrs: {
src = fetchurl {
url = "https://example.com/sl.tar.gz";
sha256 = "sha256-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
};
})
Les overrides sont l'une des fonctionnalités les plus puissantes de Nix (de Nixpkgs en vérité), et c'est ce qui permet de faire des choses que seules les distributions comme Gentoo permettent de faire facilement. Mais contrairement à ces dernières, pas besoin pour ça de devoir compiler tout son système : Nix ne recompile que ce qui est nécessaire.
D'où viennent ces fonctions ?
overrideAttrs
est ajouté aux dérivations parstdenv.mkDerivation
, et définie dans stdenv/generic/make-derivation.nix (la fonction passée àmkDerivationSimple
sera ajoutée aux dérivations en tant queoverrideAttrs
).override
est ajouté aux dérivations parcallPackage
, grâce à la fonctionmakeOverridable
définie dans lib/customisation.nix (c'est aussi dans ce fichier qu'est définiecallPackageWith
, fonction servant à créercallPackage
).
makeOverridable
est une fonction assez complexe, devant être capable définir
une fonction override
sur le résultat d'une fonction, capable de modifier les
entrées de ladite fonction (bizarre non ?).
makeOverridable
s'utilise sur des fonctions renvoyant un set (forcément, il
faut pouvoir y ajouter notamment l'attribut override
), et prend en paramètre
la fonction à rendre overridable, ainsi qu'un set correspondant aux paramètres
initiaux de la fonction (que l'on va pouvoir surcharger), par exemple :
myAdd = { a ? 1, b ? 2 }: { result = a + b; }
myAddOverridable = makeOverridable f { a = 1; b = 2; }
Essayez de jouer avec myAddOverridable
, vous verrez notamment que dans le set
qu'elle renvoie, il y aura un attribut override
.
Mais du coup, vu qu'
override
est dans le résultat de la fonction, cela veut dire qu'il faut d'abord l'appeler pour pouvoir l'utiliser, et que si on l'override, on l'appellera deux fois ?
Oui ! Mais ce n'est pas censé être un problème. En effet, même si la fonction
renvoyant la dérivation est appelée deux fois, lors du premier appel, nous
n'utiliserons que le champ override
, et la dérivation ne sera donc pas évaluée
en entier. Vous pouvez facilement le vérifier à l'aide de builtins.trace
,
essayez de lancer cet exemple dans le REPL :
f = { suffix }: stdenv.mkDerivation { name = builtins.trace "I'm called!" "example${suffix}"; }
fOverridable = makeOverridable f { suffix = ""; }
fOverridable.override { suffix = "-2"; }
Bonus : installer des paquets depuis deux versions de NixOS
Si vous êtes sur NixOS stable, il est possible qu'une version majeure d'un paquet sorte et que vous vouliez l'utiliser. Malheureusement, étant une version majeure, elle ne sera disponible que sur la prochaine version de NixOS, et se retrouve sur unstable en attendant.
Il est possible malgré tout de l'installer, et ça sans avoir à compiler quoi que ce soit ou à changer de version de NixOS !
L'exemple suivant ne marchera que si vous êtes sur NixOS et en stable, mais cette méthode reste possible même si ce n'est pas le cas et peut servir dans beaucoup d'autres situations. Je n'ai juste pas trouvé d'autre exemple assez parlant.
Commencez par ajouter un channel pour nixos-unstable
, vous pouvez le nommer
comme vous voulez, ici je l'appelle unstable
:
J'utilise sudo
pour ajouter le channel au système, mais vous pouvez l'omettre
si vous ne voulez l'ajouter que pour votre utilisateur.
Cela permettra aussi que le nix-channel --update
qui suit n'affecte pas votre
channel nixos
et ne mette donc pas à jour votre channel principal, qui
entraînerait une mise à jour de votre système au prochain appel à
nixos-rebuild
(voir la page sur NixOS).
sudo nix-channel --add https://nixos.org/channels/nixos-unstable unstable
sudo nix-channel --update
Vous pouvez désormais installer un paquet depuis unstable
, par exemple
nodejs
:
nix-env -iA unstable.nodejs
Nix permet d'avoir plusieurs versions d'un même paquet dans le store, mais il ne permet pas de les utiliser en même temps : seule l'une de ces versions peut être dans l'environnement utilisateur actif.
Si vous aviez déjà installé nodejs
via nix-env
, vous ne pourrez pas
installer la nouvelle version tant que vous n'aurez pas supprimée la première,
sinon Nix vous affichera une erreur de conflit de fichier lorsqu'il essaiera de
créer les liens symboliques de l'environnement.
Mais du coup, à quoi ça sert que Nix permette d'avoir plusieurs versions du même paquet dans le store ?
Cela peut servir, entre autre :
- Lorsque, sur NixOS, vous voulez installer une version différente dans votre environnement utilisateur de celle qui est installée dans l'environnement système.
- Mais surtout, ça sert pour les
nix-shell
!
Bonus : la commande nix-shell
Dans la longue liste des commandes que Nix propose, il y en a une
particulièrement utile : nix-shell
.
nix-shell
permet de lancer un shell (bash par défaut) avec des paquets
spécifiques de temporairement disponibles à l'intérieur.
Par exemple, imaginons que vous vouliez utiliser nodejs
pour un projet, mais
que vous ne voulez pas l'installer dans votre environment. Vous pouvez faire :
nix-shell -p nodejs
Cela devrait vous ouvrir un shell avec Node.JS (commandes node
et npm
) de
disponible dedans, mais seulement dedans ! Si vous quittez le shell, les
commandes ne sont plus accessibles.
Vous pouvez aussi utiliser un nix-shell
pour utiliser une version différente
d'un paquet que vous avez déjà installé. Par exemple, si vous avez la dernière
version de Java installée via nix-env
mais qu'un ancien projet a besoin de la
version 8, il vous suffit d'utiliser :
nix-shell -p jdk8
Vous pourrez compiler et lancer votre projet à l'intérieur sans problème !
Les fichiers shell.nix
nix-shell
peut aussi utiliser un fichier shell.nix
pour définir les paquets
à utiliser (et plus !), ce qui est très pratique notamment pour les projets. Il
n'est pas rare de voir des shell.nix
à la racine des dépôts git.
Par exemple, pour un projet Java, on peut avoir :
with import <nixpkgs> { };
mkShell {
buildInputs = [
jdk8
maven
];
}
mkShell
est une fonction de Nixpkgs permettant de créer une dérivation exprès
pour un shell. Elle peut prendre bien plus que des buildInputs
, par exemple un
shellHook
permettant de définir une commande à lancer lors de l'entrée dans le
shell.
Lorsqu'un fichier shell.nix
est disponible dans un dossier, il suffit de faire
nix-shell
sans argument pour le charger.
Par défaut, nix-shell
lancé sans argument ira chercher le fichier shell.nix
dans le dossier courant. S'il n'existe pas, il ira chercher default.nix
à la
place.
Il est aussi possible de manuellement lui préciser le chemin du fichier.
Syntaxe
La façon la plus facile d'utiliser nix-shell
est de passer des paquets à
l'option -p
. Cette dernière suit relativement la même syntaxe que
nix-env -iA
mais sans le nixpkgs.
(ou nixos.
) devant le chemin qui est
automatiquement ajouté.
Tout comme nix-env -iA
, l'option -p
accepte possiblement plusieurs paquets à
la suite.
Si on veut se baser sur un channel différent de nixpkgs
(ou nixos
), on peut
utiliser la syntaxe suivante
nix-shell '<channel>' -p paquet1 paquet2...
Le comportement de nix-shell
est d'ouvrir un shell avec comme paquets
disponibles, les dépendances d'une certaine dérivation. Il est donc pratique de
l'utiliser lors du développement d'un paquet.
L'option -p
créé en vérité une dérivation temporaire avec comme dépendances
les paquets passés en paramètre, en y collant littéralement les arguments de
-p
dans la liste des buildInputs
dans un contexte de with import <nixpkgs>
(d'où le fait qu'il n'y ait pas besoin de préciser nixpkgs.
ou nixos.
comme
avec nix-env
).
On ne va pas trop s'étayer dessus, mais la commande est très flexible et permet
de faire beaucoup de choses. Je vous invite à jeter un œil à la documentation
complète de nix-shell
qui est disponible
à cette adresse.
Bonus : Utiliser autre chose que bash
Par défaut, nix-shell
ouvre un shell bash. La façon la plus simple de lui en
faire ouvrir un autre est d'utiliser le paramètre --run
permettant de lui
spécifier une commande à lancer à l'ouverture du shell, en mettant simplement
votre propre shell.
Par exemple :
nix-shell -p jdk maven --run fish
Sinon, il est aussi possible d'utiliser
any-nix-shell
qui permet de se
coller à votre shell pour automatiquement faire en sorte que nix-shell
lance
le même shell que dans lequel vous êtes.
Bonus : Faire persister son shell
Par défaut, nix-shell
va créer un environnement temporaire, qui sera détruit
au prochain passage du garbage collector. Lorsque vous lancerez votre shell à
nouveau, il faudra donc à nouveau que Nix télécharge ses dépendances.
Il existe un moyen de faire persister un shell dans le store. Un shell étant une dérivation, il suffit d'ajouter cette dérivation à la liste des racines du garbage collector.
Pour ça, nous devons d'abord activer l'option keep-outputs
de Nix, qui lui dit
de garder les outputs des dérivations même si lesdits outputs ne sont pas
utilisés (mais que les dérivations le sont) :
- Nix
- NixOS
Ajoutez la ligne keep-outputs = true;
dans votre fichier de configuration Nix
/etc/nix/nix.conf
(créez le fichier s'il n'existe pas).
Ajoutez la ligne nix.settings.keepOutputs = true;
dans votre configuration
système, car le fichier /etc/nix/nix.conf
est généré par Nix et est donc en
lecture seule (et écrasé à chaque fois). Lancez ensuite
sudo nixos-rebuild switch
.
Si vous êtes perdu, jetez d'abord un oeil à la partie du cours sur NixOS.
Ensuite, nous pouvons utiliser la commande nix-instantiate
pour évaluer la
dérivation, avec le paramètre --add-root
pour lui dire d'ajouter la dérivation
aux racines du garbage collector et --indirect
pour l'avoir dans un dossier
hors du dossier des racines (comme les fichiers result
) :
nix-instantiate --add-root shell.drv --indirect shell.nix
Un fichier shell.drv
devrait apparaître dans le dossier courant. C'est un lien
vers la dérivation de votre shell, et il est enregistré comme racine. De ce
fait, tant que vous ne supprimez (ou ne déplacez) pas le fichier, Nix gardera
ses dépendances et donc les dépendances de votre shell.
Les outputs
Nix permet de définir plusieurs outputs pour une dérivation. Par exemple, le
paquet openssl
a 6 outputs :
bin
: ses programmes (commandesopenssl
etc_rehash
)debug
: la version de ses bibliothèques dynamiques avec les symboles servant au debug (celui-ci n'est pas présent sur macOS)dev
: ses headers, fichiers de configurationpkg-config
, etc.doc
: sa documentationman
: ses pages de manuelman
out
: l'output par défaut avec contenu principal de la dérivation, ici ses bibliothèques dynamiques (libcrypto
,libssl
, etc.)
Les dérivations ne sont pas obligées d'avoir différents outputs, par défaut seul
l'output out
est présent et certaines dérivations mettent tout à l'intérieur.
Cependant, définir plusieurs outputs permet de séparer les usages et de pouvoir
n'en sélectionner que certains.
Par exemple, si vous installez openssl
via nix-env
, seuls les outputs bin
et man
seront ajoutés à votre environnement. De ce fait, vous ne pourrez pas
utiliser les headers de la librairie en passant par nix-env
.
Heureusement, nix-shell
arrive à la rescousse ! Si vous ouvrez un shell avec
openssl
en paramètre, nix-shell
va intelligemment utiliser chacun des
outputs :
- Les binaires d'OpenSSL seront ajoutés au
PATH
- Les paramètres d'inclusion des headers pour un compilateur C seront ajoutés à
la variable
NIX_CFLAGS_COMPILE
- Les paramètres d'inclusion des bibliothèques pour
ld
seront ajoutés à la variableNIX_LD_FLAGS
- Si
pkg-config
fait partie de votre shell, lePKG_CONFIG_PATH
sera défini de sorte qu'il puisse trouver les fichiers de configuration d'OpenSSL - Si
cmake
fait partie de votre shell, les variablesCMAKE_PREFIX_PATH
etCMAKE_INCLUDE_PATH
seront aussi définies comme il faut - etc.
Pour développer avec Nix, il faut donc impérativement passer par nix-shell
et
non par nix-env
. C'est une erreur courante de passer par ce dernier et de se
retrouver avec son compilateur qui rale. Il faut aussi penser à utiliser un
outil comme pkg-config
, cmake
, autotools
, ou autre, ou utiliser les
variables d'environnement mises à disposition (NIX_CFLAGS_COMPILE
,
NIX_LD_FLAGS
, etc.).
Pour manipuler les outputs en Nix, il y a plusieurs choses à savoir :
- Il est possible d'accéder à la liste des outputs d'une dérivation via son
attribut
outputs
. - Il est possible de savoir quels outputs seront installés par
nix-env
pour une dérivation via son attributmeta.outputsToInstall
. - Chaque output est une dérivation à part entière accessible directement en tant
qu'attribut de la dérivation principale, par exemple
openssl.dev
. - Vous vous rappelez du
$out
accessible dans les scripts de build d'une dérivation ? Il y en a en vérité un pour chaque output, par exemple$dev
,$bin
, etc.
Pour en savoir plus sur les outputs, je vous invite à lire la documentation de Nixpkgs à ce sujet.
Il peut être tentant d'installer un certain output d'une dérivation avec
nix-env
en utilisant par exemple :
nix-env -iA nixpkgs.openssl.dev
Même si la commande aura l'air de fonctionner, nix-env
aura en vérité ignoré
l'output spécifié et installé les outputs définis par
openssl.meta.outputsToInstall
comme initialement.
Le seul moyen d'installer des outputs spécifiques est d'utiliser un override de
la dérivation pour changer outputsToInstall
, comme indiqué dans la
documentation que je viens de citer. Vous pouvez appliquer l'override dans une
expression que vous utiliserez avec nix-env -iE
, ou dans un overlay comme
expliqué juste après.
Les overlays
Il peut être pratique d'appliquer des modifications à Nixpkgs, par exemple pour appliquer un override ou ajouter son propre paquet, sans pour autant fork le dépôt. C'est pour ça que Nix propose les overlays.
Un overlay a la forme d'une double fonction prenant respectivement :
self
: la cible originale (Nixpkgs dans notre cas) sur laquelle sera appliqué l'overlaysuper
: la cible telle qu'elle a été modifiée par les précédents overlay
Il peut être difficile de comprendre l'intérêt de super
, pour ça il faut
comprendre que les overlay sont appliqués à la chaîne avec l'opérateur //. Dans
la chaîne de fusion, super
correspond à l'état intermédiaire Nixpkgs sur
lequel tous les overlays précédent celui-ci dans la chaîne ont été appliqués,
alors que self
représente le Nixpkgs original.
De ce fait, il vaut mieux appliquer nos modifications sur super
pour éviter
d'effacer les modifications des autres overlays.
Voici un exemple d'overlay définissant un paquet google-chrome-unsafe
ayant
ses fonctionnalités de sécurité de désactivées :
self: super: {
google-chrome-unsafe = super.google-chrome.override {
commandLineArgs = "--disable-web-security";
};
}
Il aurait aussi possible d'appeler notre paquet google-chrome
, remplaçant
alors l'original.
Il existe plusieurs moyens d'appliquer un overlay en fonction du contexte :
- En les écrivant dans un fichier
.nix
dans le dossier~/.config/nixpkgs/overlays
(le nom du fichier n'a pas d'importance), ou dans un dossier enregistré avec l'aliasnixpkgs-overlay
dans leNIX_PATH
- En les mettant à l'intérieur de l'attribut
overlays
du paramètre de la fonction d'initialisation de Nixpkgs :let
overlay = self: super: { /* ... */ };
nixpkgs = import <nixpkgs> { overlays = [ overlay ]; };
in
nixpkgs.X - Et plus encore ! Jetez un coup d'œil à la page du wiki non-officiel pour en savoir plus
Après avoir enregistré notre exemple d'overlay précédent en le mettant, par
exemple, dans le fichier ~/.config/nixpkgs/overlays/google-chrome-unsafe.nix
,
il est possible d'installer notre nouveau paquet via nix-env
:
- Nix
- NixOS
nix-env -iA nixpkgs.google-chrome-unsafe
nix-env -iA nixos.google-chrome-unsafe
Noter paquet exposant les mêmes fichiers que l'original, il ne sera pas possible
d'installer les deux à la fois sans conflit. Si c'est ce que nous voulions, nous
aurions pu par exemple modifier la phase postFixup
via overrideAttrs
pour
renommer ses fichiers au passage.
Conclusion
Nix est un gestionnaire de paquet très particulier.
À première vue, on peut l'utiliser comme n'importe quel autre gestionnaire de
paquet en utilisant simplement nix-env
sans se poser de question, mais il est
en réalité bien plus puissant et complexe que ça.
On a vu que Nix permettait, entre autre :
- Que chaque utilisateur puisse gérer ses paquets de manière indépendante, sans avoir besoin de droit particulier, le tout sans containerisation (coucou snap) et en utilisant un store partagé réutilisé par tout le monde.
- De gérer des paquets sans toucher au système, en étant isolé dans son dossier
/nix
et en utilisant simplement des liens à des endroits très spécifiques et toujours à lui. - De revenir en arrière dans le temps grâce à son système de générations.
- De pouvoir créer ses propres paquets facilement (en théorie).
- De permettre d'entièrement personnaliser les paquets qu'on installe, sans pour autant avoir besoin de tout compiler : seul le nécessaire le sera.
- De pouvoir tout décrire grâce à un langage de programmation puissant, ce qui permet notamment de partager facilement des paquets et même plus (shells, patchs, ...).
- De créer des
shells
permettant de séparer les paquets utilisés par un projet de ceux utilisés par le système. - D'installer des paquets d'autres versions de NixOS sans problème.
- D'isoler les paquets entre eux pour éviter tout conflit de quelconque nature.
- De s'installer à côté de n'importe quel autre gestionnaire de paquet sans risque de conflit.
- Tout ça et bien plus, en étant pourtant très rapide !
Évidemment, Nix a aussi ses limitations. En plus d'être difficile à apprendre, il y a des paquets qui ne sont malheureusement pas disponibles dans ses dépôts. De plus, il peut être difficile d'ajouter des programmes complexes à Nix, et il acceptera difficilement quelque chose qui n'est pas parfaitement défini dans une dérivation.
Malgré ça, il reste un très bon choix pour gérer ses paquets, surtout sur macOS
(où les seules alternatives sont claquées au sol on va pas se mentir), mais
aussi sur Linux pour facilement gérer les dépendances de ses projets, surtout à
plusieurs où il est pratique de se passer un shell.nix
.
N'hésitez pas à parcourir la documentation disponible, notamment :
- Le manuel de Nix
- Le manuel de Nixpkgs
- Le wiki non-officiel
- Internet en général, il y a beaucoup de ressources disponibles et la documentation officielle est encore un peu fraîche.
Nix est assez difficile à comprendre entièrement, nous n'attendons pas de vous d'avoir parfaitement tout saisi. Mais n'hésitez pas à garder les deux pages précédentes du cours sous la main, surtout pour la suite.