Skip to main content

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 de nixpkgs-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.

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 vers https://nixos.com/channels/nixos-XX.XX, le XX.XX correspondant à la version de NixOS que vous avez installée (qui peut donc être unstable).
  • Sur Nix, un channel utilisateur (donc sur l'utilisateur ayant installé Nix) a été ajouté appelé nixpkgs et pointant vers https://nixos.org/channels/nixpkgs-unstable. Il est normal d'utiliser le channel unstable, é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 ou nixpkgs-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.
Rappel

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.

Attention

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.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.

Attention

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.

Conseil

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 :

REPL
:l <nixpkgs>
lib.version
Info

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.

Pour aller plus loin

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érivation
  • src : Les sources de la dérivation
  • buildInputs : 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érivation
  • installPhase : Le script d'installation de la dérivation
  • meta : Les métadonnées de la dérivation (mainteneur, license, etc.)
  • et bien plus...
Pour information

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 :

hello.nix
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 non builtins.fetchGit pour télécharger les sources du projet. La différence est que builtins.fetchGit télécharge depuis git au moment de l'évaluation, alors que nixpkgs.fetchgit le fait au moment du build. Celle de builtins 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ôt nixpkgs.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 utiliser nixpkgs.fetchurl pour télécharger des sources depuis une URL plutôt que builtins.fetchurl.
  • buildPhase et installPhase 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 variable PREFIX 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. Utiliser master 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 :

Pour build notre dérivation avec nix-build, exécutez simplement la commande suivante dans le terminal :

nix-build 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.

Petit exercice

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.

Info

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...).
  • 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.

Ce qu'il faut retenir

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 :

hello-autotools.nix
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 (comme x86_64-linux ou aarch64-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 script default-builder.sh, ce dernier appelant simplement le setup ainsi que sa fonction genericBuild.
  • 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.

Remarque

Il serait très bien possible d'écrire un builder dans un autre langage, comme Python !

Info

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-env -iA nixpkgs.sl
Info

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 :

Installation de sl avec nix-env
Info

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 (ou nixos.sl), correspondant à l'attribut sl de l'évaluation du channel nixpkgs (ou nixos). 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.

Pour aller plus loin : le cache

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.

Pour aller plus loin : les default expressions

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 via nix-env à la fois (cela prendra aussi un petit moment le temps que Nix évalue tout, mais ça peut valoir le coup).
Attention

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.

Conseil

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).

Info

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 :

Exemple de sortie de otool -L sur un binaire Nix

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.

Pour aller plus loin

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 symbolique result vers le .drv dans les racines du garbage collector, évitant qu'elle ne soit supprimée par ce dernier.
  • --indirect aurait créé le lien result dans le dossier courant en plus de celui dans le dossier des racines, ce dernier pointant vers result qui lui pointe vers la dérivation. De ce fait, la suppression de result 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 :

https://github.com/NixOS/nixpkgs/blob/master/pkgs/tools/misc/sl/default.nix
{ 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.

Pour aller plus loin

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 :

my-sl.nix
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 :

my-other-sl.nix
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.

Pour aller plus loin

D'où viennent ces fonctions ?

  • overrideAttrs est ajouté aux dérivations par stdenv.mkDerivation, et définie dans stdenv/generic/make-derivation.nix (la fonction passée à mkDerivationSimple sera ajoutée aux dérivations en tant que overrideAttrs).
  • override est ajouté aux dérivations par callPackage, grâce à la fonction makeOverridable définie dans lib/customisation.nix (c'est aussi dans ce fichier qu'est définie callPackageWith, fonction servant à créer callPackage).

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 !

Attention

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 :

Conseil

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
Attention : risque de conflit

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.

Pour aller plus loin

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...
Pour aller plus loin (encore)

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) :

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).

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 (commandes openssl et c_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 configuration pkg-config, etc.
  • doc : sa documentation
  • man : ses pages de manuel man
  • 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 variable NIX_LD_FLAGS
  • Si pkg-config fait partie de votre shell, le PKG_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 variables CMAKE_PREFIX_PATH et CMAKE_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 attribut meta.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.

Attention

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'overlay
  • super : 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";
};
}
Remarque

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'alias nixpkgs-overlay dans le NIX_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-env -iA nixpkgs.google-chrome-unsafe
Attention

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 :

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.